Initial commit — Performance West telecom compliance platform

Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-27 06:54:22 -05:00
commit f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions

11
site/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

4
site/astro.config.mjs Normal file
View file

@ -0,0 +1,4 @@
import { defineConfig } from "astro/config";
export default defineConfig({
server: { host: "0.0.0.0", port: 4321 },
});

39
site/nginx.conf Normal file
View file

@ -0,0 +1,39 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
error_page 404 /404.html;
location / {
try_files $uri $uri/index.html $uri.html =404;
}
location /api/ {
proxy_pass http://api:3001/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Astro's hashed _astro/ files content-addressed, safe to cache forever
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Images + fonts long cache but NOT immutable (can be updated)
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public";
}
# HTML pages short cache so deploys take effect quickly
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
# Security headers are set on the outer (host) nginx do NOT duplicate here
}

5475
site/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

13
site/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "pw-site",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "astro dev --port 4321",
"build": "astro build",
"check": "astro check"
},
"dependencies": {
"astro": "^4.16.0"
}
}

67
site/public/404.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
import"./hoisted.yFz1BYXO.js";const r=document.getElementById("contact-support-btn");r&&r.addEventListener("click",()=>{const o=document.getElementById("support-fab");o&&window.setTimeout(()=>o.click(),0)});const t=document.getElementById("contact-form"),n=document.getElementById("form-status"),s=document.getElementById("submit-btn");t&&t.addEventListener("submit",async o=>{if(o.preventDefault(),t.querySelector('[name="website"]')?.value)return;s.disabled=!0,s.textContent="Sending...",n.classList.add("hidden");const e=new FormData(t),a={title:e.get("subject"),group:"Users",customer:e.get("email"),article:{subject:e.get("subject"),body:`Name: ${e.get("name")}
Company: ${e.get("company")||"N/A"}
Phone: ${e.get("phone")||"N/A"}
${e.get("message")}`,type:"web",sender:"Customer",from:e.get("email")}};try{if((await fetch((window.__PW_API||"https://api.performancewest.net")+"/api/v1/tickets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).ok)n.textContent="Message sent successfully. We'll get back to you within 1 business day.",n.className="rounded-lg p-4 text-sm bg-green-50 text-green-800 border border-green-200",t.reset();else throw new Error("Server error")}catch{n.textContent="Something went wrong. Please try again or email us directly at info@performancewest.net.",n.className="rounded-lg p-4 text-sm bg-red-50 text-red-800 border border-red-200"}finally{s.disabled=!1,s.textContent="Send message"}});

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";const i=window.__PW_API||"https://api.performancewest.net";function c(t){return new URLSearchParams(window.location.search).get(t)??""}const d=c("order_id"),l=c("order_type")||"canada_crtc",m=c("expedited")==="1",s=document.getElementById("cancelled-order-id");s&&d&&(s.textContent=`Order ID: ${d}`);if(m){document.querySelectorAll("#retry-methods > label").forEach(r=>{const n=r.querySelector("input[type='radio']");n&&n.value!=="paypal"&&n.value!=="crypto"&&(r.classList.add("hidden"),n.checked=!1)});const t=document.querySelector('input[name="retry_method"][value="paypal"]');t&&(t.checked=!0);const e=document.getElementById("expedited-retry-note");e&&e.classList.remove("hidden")}document.getElementById("btn-retry-payment")?.addEventListener("click",async()=>{const t=document.getElementById("btn-retry-payment"),e=document.getElementById("retry-status"),r=document.querySelector('input[name="retry_method"]:checked')?.value||"card";t.disabled=!0,t.textContent="Redirecting...",e&&(e.textContent="Creating checkout session...",e.classList.remove("hidden"));try{const n=await fetch(i+"/api/v1/checkout/create-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:d,order_type:l,payment_method:r})}),o=await n.json(),a=o.url||o.checkout_url||o.redirect_url;if(n.ok&&a)e&&(e.textContent="Redirecting to payment..."),window.location.href=a;else throw new Error(o.error||"Failed to create checkout session")}catch(n){e&&(e.className="mt-3 text-sm text-red-600 text-center",e.textContent=n.message||"Something went wrong. Please try again.",e.classList.remove("hidden")),t.disabled=!1,t.innerHTML='<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/></svg> Retry Payment'}});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
import"./hoisted.yFz1BYXO.js";const p=window.__PW_API||"",b=new URLSearchParams(window.location.search),d=b.get("token")||sessionStorage.getItem("pw_esign_token")||"";d&&sessionStorage.setItem("pw_esign_token",d);const w=()=>d?{Authorization:`Bearer ${d}`,"Content-Type":"application/json"}:{"Content-Type":"application/json"},i=t=>document.getElementById(t),g=i("loading-overlay"),k=i("main-ui"),L=i("success-screen"),P=i("hdr-entity"),S=i("entity-name-inline"),_=i("entity-name-confirm"),E=i("letter-preview-container"),f=i("letter-loading"),y=i("agree-chk"),l=i("submit-btn"),o=i("status-msg"),n=i("sig-canvas"),R=i("sig-clear"),C=i("sig-hint"),T=i("already-signed-card"),N=i("signed-at-display"),s=n.getContext("2d");let c=!1,u=!1;function h(t){const e=n.getBoundingClientRect(),r=n.width/e.width,a=n.height/e.height,v=t instanceof TouchEvent?t.touches[0]:t;return{x:(v.clientX-e.left)*r,y:(v.clientY-e.top)*a}}function H(){const t=n.getBoundingClientRect();n.width=t.width*devicePixelRatio,n.height=t.height*devicePixelRatio,s.scale(devicePixelRatio,devicePixelRatio),s.strokeStyle="#1e3a5f",s.lineWidth=2.5,s.lineCap="round",s.lineJoin="round"}n.addEventListener("mousedown",t=>{c=!0;const e=h(t);s.beginPath(),s.moveTo(e.x,e.y)});n.addEventListener("mousemove",t=>{if(!c)return;const e=h(t);s.lineTo(e.x,e.y),s.stroke(),x()});n.addEventListener("mouseup",()=>{c=!1});n.addEventListener("mouseleave",()=>{c=!1});n.addEventListener("touchstart",t=>{t.preventDefault(),c=!0;const e=h(t);s.beginPath(),s.moveTo(e.x,e.y)},{passive:!1});n.addEventListener("touchmove",t=>{if(t.preventDefault(),!c)return;const e=h(t);s.lineTo(e.x,e.y),s.stroke(),x()},{passive:!1});n.addEventListener("touchend",()=>{c=!1});function x(){u||(u=!0,n.classList.add("has-sig"),C.textContent="Signature drawn ✓",m())}R.addEventListener("click",()=>{s.clearRect(0,0,n.width/devicePixelRatio,n.height/devicePixelRatio),u=!1,n.classList.remove("has-sig"),C.textContent="Draw your signature above",m()});function m(){l.disabled=!(u&&y.checked)}y.addEventListener("change",m);async function I(){if(!d){g.innerHTML=`<div style="color:#dc2626;font-weight:600">
Invalid or expired link.<br>
<span style="font-size:.875rem;font-weight:400;color:#64748b">
Please use the link from your Performance West email. Links expire after 72 hours.
</span>
</div>`;return}try{const t=await fetch(`${p}/api/v1/portal/esign-info`,{headers:w()});if(!t.ok){const a=await t.json().catch(()=>({error:"Request failed"}));g.innerHTML=`<div style="color:#dc2626">${a.error||"Failed to load. Please try the link again."}</div>`;return}const e=await t.json(),r=e.entity_name||e.customer_name;if(P.textContent=`Order ${e.order_number} ${r}`,S.textContent=r,_.textContent=r,g.style.display="none",k.style.display="block",e.letter_preview_url){f.style.display="none";const a=document.createElement("iframe");a.src=e.letter_preview_url,a.className="letter-frame",a.title="CRTC Notification Letter",a.setAttribute("allowfullscreen",""),E.appendChild(a)}else f.innerHTML=`
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="margin-bottom:.5rem"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
Letter preview unavailable please continue with signing.
`;if(e.already_signed&&e.signed_at){T.style.display="block",document.querySelectorAll(".card:not(#already-signed-card)").forEach(a=>a.style.display="none"),N.textContent=new Date(e.signed_at).toLocaleString("en-CA",{dateStyle:"long",timeStyle:"short"});return}H()}catch{g.innerHTML='<div style="color:#dc2626">Network error — please refresh and try again.</div>'}}l.addEventListener("click",async()=>{if(!u){o.textContent="Please draw your signature above.",o.className="err";return}if(!y.checked){o.textContent="Please confirm that you have read the letter.",o.className="err";return}l.disabled=!0,o.textContent="Submitting signature…",o.className="info";const t=n.toDataURL("image/png");try{const e=await fetch(`${p}/api/v1/portal/esign-submit`,{method:"POST",headers:w(),body:JSON.stringify({signature_png:t,agreed:!0})}),r=await e.json();if(!e.ok||!r.success){o.textContent=r.error||"Submission failed. Please try again.",o.className="err",l.disabled=!1;return}k.style.display="none",L.style.display="block",sessionStorage.removeItem("pw_esign_token")}catch{o.textContent="Network error. Please check your connection and try again.",o.className="err",l.disabled=!1}});I();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
import"./hoisted.yFz1BYXO.js";const h=window.__PW_API||"https://api.performancewest.net";function m(n){return new URLSearchParams(window.location.search).get(n)??""}const a=m("order_id"),f=m("order_type")||"canada_crtc",l=m("amount"),w=m("expedited"),b=w==="1"?"&expedited=1":"",p=`/order/cancelled?order_id=${a}&order_type=${f}${b}`,u=document.getElementById("btn-back");u&&a&&(u.href=p);const y=document.getElementById("error-retry-link");y&&a&&(y.href=p);const g=document.getElementById("btn-change-method");g&&a&&(g.href=p);function d(n){for(const t of["loading","pick","pay","error"]){const e=document.getElementById(`state-${t}`);e&&e.classList.toggle("hidden",t!==n)}}!a||!l?d("error"):(async()=>{try{const n=document.getElementById("fiat-total");n&&(n.textContent=`$${parseFloat(l).toLocaleString(void 0,{minimumFractionDigits:2,maximumFractionDigits:2})} USD`);const t=await fetch(`${h}/api/v1/checkout/crypto-available`);if(!t.ok)throw new Error("Gateway unavailable");const e=await t.json(),o={BTC:"bitcoin",ETH:"ethereum",LTC:"litecoin",DOGE:"dogecoin",BNB:"bnb",MATIC:"polygon",TRX:"tron"},r=document.getElementById("crypto-list");for(const s of e.cryptos){const i=document.createElement("label");i.className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 cursor-pointer hover:border-pw-400 has-[:checked]:border-pw-500 has-[:checked]:bg-pw-50 transition-colors";const k=s.name.split("-")[0].toLowerCase();i.innerHTML=`
<input type="radio" name="crypto_choice" value="${s.name}" class="text-pw-600" />
<span class="text-sm font-medium text-gray-800">${s.display_name}</span>
<span class="ml-auto text-xs text-gray-400 font-mono">${s.name}</span>
`,i.addEventListener("click",()=>{document.getElementById("pick-error")?.classList.add("hidden")}),r.children.length===0&&(i.querySelector("input").checked=!0),r.appendChild(i);const x=document.createElement("button")}const c=document.createElement("button");c.type="button",c.className="mt-4 w-full inline-flex items-center justify-center gap-2 bg-pw-700 hover:bg-pw-800 text-white font-medium px-5 py-3 rounded-lg transition-colors text-sm disabled:opacity-50 disabled:cursor-not-allowed",c.innerHTML='Continue <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>',c.addEventListener("click",()=>E(c)),r.parentElement.appendChild(c),d("pick")}catch{document.getElementById("error-message").textContent="Could not load available cryptocurrencies. The payment gateway may be temporarily offline.",d("error")}})();async function E(n){const t=document.querySelector('input[name="crypto_choice"]:checked')?.value;if(!t){const e=document.getElementById("pick-error");e&&(e.textContent="Please select a cryptocurrency.",e.classList.remove("hidden"));return}n.disabled=!0,n.textContent="Generating payment address...";try{const e=await fetch(`${h}/api/v1/checkout/crypto-invoice`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:a,order_type:f,crypto_name:t,amount:l})});if(!e.ok){const r=await e.json().catch(()=>({}));throw new Error(r.error||"Invoice creation failed")}const o=await e.json();document.getElementById("pay-crypto-badge").textContent=o.display_name||t,document.getElementById("pay-amount").textContent=`${o.amount} ${t}`,document.getElementById("pay-fiat").textContent=`$${parseFloat(l).toLocaleString(void 0,{minimumFractionDigits:2})} USD`,document.getElementById("pay-wallet").textContent=o.wallet,document.getElementById("pay-network-warn").textContent=o.display_name||t,document.getElementById("pay-order-id").textContent=a,o.exchange_rate?document.getElementById("pay-rate").textContent=`1 ${t.split("-")[0]} = $${parseFloat(o.exchange_rate).toLocaleString()} USD`:document.getElementById("rate-row")?.classList.add("hidden"),document.getElementById("btn-copy")?.addEventListener("click",()=>{navigator.clipboard.writeText(o.wallet).then(()=>{const r=document.getElementById("btn-copy");r&&(r.title="Copied!",setTimeout(()=>{r.title="Copy address"},2e3))})}),d("pay")}catch(e){const o=document.getElementById("pick-error");o&&(o.textContent=e.message||"Failed to generate payment address.",o.classList.remove("hidden")),n.disabled=!1,n.innerHTML='Continue <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>'}}document.getElementById("btn-change-coin")?.addEventListener("click",()=>{const n=document.querySelectorAll("#crypto-list ~ button");for(const t of n){const e=t;e.disabled=!1,e.innerHTML='Continue <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>'}d("pick")});

View file

@ -0,0 +1,25 @@
import"./hoisted.yFz1BYXO.js";const d=[{id:1,text:"Do you control when the worker works?",detail:"You set the worker's schedule, hours, or specific times they must be available."},{id:2,text:"Do you control how the work is done?",detail:"You dictate the methods, processes, or procedures the worker must follow — not just the end result."},{id:3,text:"Do you provide tools or equipment?",detail:"You supply the worker with computers, software, vehicles, or other tools needed to perform the work."},{id:4,text:"Does the worker work exclusively for you?",detail:"The worker performs services only for your company and does not have other clients."},{id:5,text:"Do you provide training to the worker?",detail:"You train the worker on how to do the job, rather than hiring someone who already has the expertise."},{id:6,text:"Is the worker integrated into your business operations?",detail:"The worker uses a company email, attends staff meetings, or is presented to clients as an employee."},{id:7,text:"Do you set the pay rate?",detail:"You determine compensation rather than the worker quoting a rate for the project or service."},{id:8,text:"Can you terminate the worker without cause?",detail:"You can end the relationship at any time without a breach of contract, similar to at-will employment."},{id:9,text:"Does the worker perform key business functions?",detail:"The worker's tasks are central to your core business rather than auxiliary or specialized services."},{id:10,text:"Do you restrict the worker from working for others?",detail:"You have a non-compete or exclusivity clause, or otherwise discourage the worker from taking other clients."}];function u(){const o=document.getElementById("questions-container");o&&(o.innerHTML=d.map((e,t)=>`
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<div class="flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-pw-100 text-pw-700 text-sm font-bold flex items-center justify-center">${e.id}</span>
<div class="flex-1">
<p class="font-medium text-gray-900 mb-1">${e.text}</p>
<p class="text-xs text-gray-500 mb-4">${e.detail}</p>
<div class="flex gap-3">
<label class="flex-1 cursor-pointer">
<input type="radio" name="q${e.id}" value="yes" class="peer sr-only" required />
<div class="peer-checked:bg-red-50 peer-checked:border-red-300 peer-checked:text-red-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
Yes
</div>
</label>
<label class="flex-1 cursor-pointer">
<input type="radio" name="q${e.id}" value="no" class="peer sr-only" required />
<div class="peer-checked:bg-green-50 peer-checked:border-green-300 peer-checked:text-green-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
No
</div>
</label>
</div>
</div>
</div>
</div>
`).join(""))}function h(){let o=0;const e=[];return d.forEach(t=>{const r=document.querySelector(`input[name="q${t.id}"]:checked`);r&&r.value==="yes"&&(o++,e.push(t.text))}),{yesCount:o,flagged:e}}function p(o,e){const t=document.getElementById("results"),r=document.getElementById("result-card"),n=document.getElementById("result-icon"),s=document.getElementById("result-title"),a=document.getElementById("result-score"),i=document.getElementById("result-description"),l=document.getElementById("result-details"),c=document.getElementById("quiz-form");!t||!r||!n||!s||!a||!i||!l||!c||(c.classList.add("hidden"),t.classList.remove("hidden"),o<=2?(r.className="rounded-xl p-8 border-2 text-center bg-green-50 border-green-300",n.textContent="✅",s.textContent="Low Risk",s.className="text-2xl font-bold mb-3 text-green-800",a.textContent=`You answered "Yes" to ${o} of 10 questions.`,a.className="text-sm font-medium mb-4 text-green-700",i.textContent="Based on your answers, your contractor relationships appear to have a low risk of misclassification. You seem to be treating your workers as independent contractors rather than employees. However, classification rules vary by state and can change. Periodic reviews are still recommended."):o<=5?(r.className="rounded-xl p-8 border-2 text-center bg-amber-50 border-amber-300",n.textContent="⚠️",s.textContent="Medium Risk",s.className="text-2xl font-bold mb-3 text-amber-800",a.textContent=`You answered "Yes" to ${o} of 10 questions.`,a.className="text-sm font-medium mb-4 text-amber-700",i.textContent="Your answers suggest some characteristics of an employer-employee relationship. This puts you in a gray area where the IRS, DOL, or state agencies could challenge your classification. You should review these relationships with a compliance professional before an audit occurs."):(r.className="rounded-xl p-8 border-2 text-center bg-red-50 border-red-300",n.textContent="🚨",s.textContent="High Risk",s.className="text-2xl font-bold mb-3 text-red-800",a.textContent=`You answered "Yes" to ${o} of 10 questions.`,a.className="text-sm font-medium mb-4 text-red-700",i.textContent="Your answers indicate a high likelihood that one or more of your contractors may be legally classified as employees. This exposes your business to back taxes, penalties, and potential lawsuits. Immediate professional review is strongly recommended."),e.length>0&&(l.innerHTML='<p class="font-medium text-gray-700 mb-2">Areas of concern:</p>'+e.map(m=>`<div class="flex items-start gap-2 text-gray-600"><span class="text-red-400 flex-shrink-0 mt-0.5"></span>${m}</div>`).join("")),t.scrollIntoView({behavior:"smooth",block:"start"}))}document.addEventListener("DOMContentLoaded",()=>{u(),document.getElementById("quiz-form")?.addEventListener("submit",e=>{e.preventDefault();let t=!0;if(d.forEach(s=>{document.querySelector(`input[name="q${s.id}"]:checked`)||(t=!1)}),!t){alert("Please answer all 10 questions before submitting.");return}const{yesCount:r,flagged:n}=h();p(r,n)}),document.getElementById("retake-btn")?.addEventListener("click",()=>{const e=document.getElementById("quiz-form"),t=document.getElementById("results");e&&t&&(e.classList.remove("hidden"),t.classList.add("hidden"),e.querySelectorAll('input[type="radio"]').forEach(n=>n.checked=!1),u(),window.scrollTo({top:0,behavior:"smooth"}))})});

View file

@ -0,0 +1,30 @@
import"./hoisted.yFz1BYXO.js";const c=[{id:1,text:"Do you have prior express written consent from every recipient?",detail:"TCPA requires prior express written consent for marketing texts. Verbal consent or implied consent is not sufficient.",risk:"Sending texts without prior express written consent is the #1 source of TCPA lawsuits. Each message can trigger $500$1,500 in statutory damages."},{id:2,text:"Is your consent form specific to your company (one-to-one consent)?",detail:"FCC's 2024 rule requires one-to-one consent — consent must be given to a single, identified seller, not shared across multiple companies.",risk:"Shared or multi-party consent forms are no longer valid under FCC rules effective January 2025. Each company must obtain its own consent."},{id:3,text:"Does your consent disclosure clearly identify you as the sender?",detail:"The consent language must clearly state your company name, the type of messages, and the number consumers will receive.",risk:"Unclear or misleading consent disclosures can void the consent entirely, leaving you liable for every message sent."},{id:4,text:"Can recipients revoke consent via text reply (STOP)?",detail:"You must honor opt-out requests immediately. Standard keywords (STOP, QUIT, CANCEL, UNSUBSCRIBE) must be supported.",risk:"Failing to honor opt-out requests converts every subsequent message into an unsolicited contact with full penalty exposure."},{id:5,text:"Do you scrub against the National DNC Registry at least every 31 days?",detail:"The FTC requires telemarketers to scrub their lists against the National Do Not Call Registry no less than every 31 days.",risk:"Contacting numbers on the DNC Registry can result in $500$1,500 per call/text, plus FTC enforcement actions."},{id:6,text:"Do you restrict messages to 8am9pm in the recipient's time zone?",detail:"TCPA prohibits calls and texts before 8:00 AM or after 9:00 PM in the recipient's local time zone.",risk:"Messages outside permitted hours are per-se TCPA violations regardless of whether the recipient consented."},{id:7,text:"Do you keep consent records for at least 5 years?",detail:"You should retain proof of consent (timestamp, IP address, consent language shown, phone number) for at least 5 years to defend against claims.",risk:"Without documented proof of consent, you cannot defend against a TCPA claim. The burden of proof is on the sender."},{id:8,text:"Did you obtain consent directly (not from a lead vendor)?",detail:"Purchasing leads with 'consent included' is risky. You are responsible for verifying the consent was properly obtained by the lead generator.",risk:"Consent obtained by a third-party lead vendor may not meet TCPA/FCC standards. You bear the liability, not the vendor."}];function f(){const e=document.getElementById("questions-container");e&&(e.innerHTML=c.map(t=>`
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<div class="flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-pw-100 text-pw-700 text-sm font-bold flex items-center justify-center">${t.id}</span>
<div class="flex-1">
<p class="font-medium text-gray-900 mb-1">${t.text}</p>
<p class="text-xs text-gray-500 mb-4">${t.detail}</p>
<div class="flex gap-3">
<label class="flex-1 cursor-pointer">
<input type="radio" name="t${t.id}" value="yes" class="peer sr-only" required />
<div class="peer-checked:bg-green-50 peer-checked:border-green-300 peer-checked:text-green-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
Yes
</div>
</label>
<label class="flex-1 cursor-pointer">
<input type="radio" name="t${t.id}" value="no" class="peer sr-only" required />
<div class="peer-checked:bg-red-50 peer-checked:border-red-300 peer-checked:text-red-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
No
</div>
</label>
</div>
</div>
</div>
</div>
`).join(""))}function h(e){return e>=1e9?"$"+(e/1e9).toFixed(1)+"B":e>=1e6?"$"+(e/1e6).toFixed(1)+"M":e>=1e3?"$"+(e/1e3).toFixed(0)+"K":"$"+e.toLocaleString()}function k(){let e=0;const t=[];return c.forEach(s=>{const n=document.querySelector(`input[name="t${s.id}"]:checked`);n&&n.value==="no"&&(e++,t.push({text:s.text,risk:s.risk}))}),{noCount:e,issues:t}}function w(e,t){const s=document.getElementById("results"),n=document.getElementById("result-card"),i=document.getElementById("result-icon"),o=document.getElementById("result-title"),r=document.getElementById("result-score"),a=document.getElementById("result-description"),u=document.getElementById("exposure-low"),m=document.getElementById("exposure-high"),d=document.getElementById("issues-detail"),p=document.getElementById("tcpa-form"),g=document.getElementById("msg-volume");if(!s||!n||!i||!o||!r||!a||!u||!m||!d||!p)return;const y=g&&parseInt(g.value)||1e4,l=8-e;p.classList.add("hidden"),s.classList.remove("hidden"),e===0?(n.className="rounded-xl p-8 border-2 text-center bg-green-50 border-green-300",i.textContent="✅",o.textContent="Compliant",o.className="text-2xl font-bold mb-3 text-green-800",r.textContent=`${l}/8 items passed. No issues identified.`,r.className="text-sm font-medium mb-4 text-green-700",a.textContent="Your SMS campaign practices appear to meet TCPA requirements based on your answers. Continue to monitor FCC rulings and maintain your consent records. Periodic professional audits can help you stay ahead of regulatory changes."):e<=2?(n.className="rounded-xl p-8 border-2 text-center bg-amber-50 border-amber-300",i.textContent="⚠️",o.textContent="At Risk",o.className="text-2xl font-bold mb-3 text-amber-800",r.textContent=`${l}/8 items passed. ${e} issue${e>1?"s":""} identified.`,r.className="text-sm font-medium mb-4 text-amber-700",a.textContent="Your campaign has compliance gaps that could expose you to TCPA litigation. Class-action TCPA lawsuits are among the most common in federal court. Address these issues promptly to reduce your risk."):(n.className="rounded-xl p-8 border-2 text-center bg-red-50 border-red-300",i.textContent="🚨",o.textContent="High Risk",o.className="text-2xl font-bold mb-3 text-red-800",r.textContent=`${l}/8 items passed. ${e} issues identified.`,r.className="text-sm font-medium mb-4 text-red-700",a.textContent="Your SMS campaign has serious compliance deficiencies. With multiple TCPA violations, you are highly vulnerable to class-action lawsuits, FCC enforcement, and substantial statutory damages. Immediate remediation and a professional consent audit are strongly recommended.");const b=y*500,v=y*1500;u.textContent=h(b),m.textContent=h(v),t.length>0?d.innerHTML='<p class="font-medium text-gray-700 mb-2">Issues identified and associated risks:</p>'+t.map(x=>`
<div class="bg-white border border-gray-200 rounded-lg p-4">
<p class="font-medium text-gray-800 text-sm mb-1">${x.text.replace(/^Do you |^Did you |^Does your |^Can |^Is your /i,C=>C)}</p>
<p class="text-xs text-gray-600">${x.risk}</p>
</div>
`).join(""):d.innerHTML="",s.scrollIntoView({behavior:"smooth",block:"start"})}document.addEventListener("DOMContentLoaded",()=>{f(),document.getElementById("tcpa-form")?.addEventListener("submit",t=>{t.preventDefault();let s=!0;if(c.forEach(o=>{document.querySelector(`input[name="t${o.id}"]:checked`)||(s=!1)}),!s){alert("Please answer all 8 questions before submitting.");return}const{noCount:n,issues:i}=k();w(n,i)}),document.getElementById("retake-btn")?.addEventListener("click",()=>{const t=document.getElementById("tcpa-form"),s=document.getElementById("results");t&&s&&(t.classList.remove("hidden"),s.classList.add("hidden"),f(),window.scrollTo({top:0,behavior:"smooth"}))})});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
import"./hoisted.yFz1BYXO.js";const f=window.__PW_API||"https://api.performancewest.net";function b(e){return new URLSearchParams(window.location.search).get(e)??""}const u=b("order"),p=b("token");p&&sessionStorage.setItem("pw_portal_token",p);const E=p||sessionStorage.getItem("pw_portal_token")||"",m={"Content-Type":"application/json"};E&&(m.Authorization=`Bearer ${E}`);let s=null,g="",l="";function i(e){for(const n of["loading","unit","did","confirm","success","error"]){const t=n==="loading"||n==="error"?"state-":"step-";document.getElementById(`${t}${n}`)?.classList.toggle("hidden",n!==e)}}u?(async()=>{try{const e=await fetch(`${f}/api/v1/portal/setup-info?order_id=${u}`,{headers:m});if(!e.ok)throw new Error("Order not found");if(s=await e.json(),s.selected_unit&&s.selected_did){i("success");return}if(s.has_own_ca_address)w(),i("did");else{const n=document.getElementById("unit-location-label");n&&s.amb_location&&(n.textContent=`Location: ${s.amb_location.name}, ${s.amb_location.city}, BC`),v(),i("unit")}}catch{i("error")}})():i("error");async function v(){try{const t=(await(await fetch(`${f}/api/v1/portal/setup-units?order_id=${u}`,{headers:m})).json()).units||[],a=document.getElementById("unit-select"),o=document.getElementById("unit-loading"),c=document.getElementById("unit-list");if(t.length===0){const d=document.getElementById("unit-error");d&&(d.textContent="No mailbox numbers available at this location right now. Please try again later or contact support.",d.classList.remove("hidden")),o&&o.classList.add("hidden");return}a.innerHTML='<option value="">Select a mailbox number...</option>';for(const d of t){const r=document.createElement("option");r.value=d,r.textContent=`Unit ${d}`,a.appendChild(r)}o&&o.classList.add("hidden"),c&&c.classList.remove("hidden")}catch{const e=document.getElementById("unit-error");e&&(e.textContent="Could not load available mailbox numbers.",e.classList.remove("hidden")),document.getElementById("unit-loading")?.classList.add("hidden")}}async function w(){try{const n=await(await fetch(`${f}/api/v1/portal/setup-dids`,{headers:m})).json(),t=document.getElementById("did-list"),a=document.getElementById("did-loading");t.innerHTML="";for(const o of n.area_codes||[]){const c=n.dids?.[o]||[];if(!c.length)continue;const d=document.createElement("div");d.innerHTML=`<p class="text-xs text-gray-500 uppercase tracking-wide font-semibold mb-2">${o} Area Code</p>`;const r=document.createElement("div");r.className="grid grid-cols-2 gap-2";for(const y of c){const h=document.createElement("label");h.className="flex items-center gap-2 p-2.5 rounded-lg border border-gray-200 cursor-pointer hover:border-pw-400 has-[:checked]:border-pw-500 has-[:checked]:bg-pw-50 transition-colors",h.innerHTML=`
<input type="radio" name="did_choice" value="${y.number}" class="text-pw-600" />
<span class="text-sm font-mono text-gray-800">${y.formatted}</span>
`,r.appendChild(h)}d.appendChild(r),t.appendChild(d)}if(a&&a.classList.add("hidden"),t.classList.remove("hidden"),!t.children.length){const o=document.getElementById("did-error");o&&(o.textContent="No available Canadian numbers found. Please contact support.",o.classList.remove("hidden"))}}catch{document.getElementById("did-loading")?.classList.add("hidden");const e=document.getElementById("did-error");e&&(e.textContent="Could not search available numbers.",e.classList.remove("hidden"))}}document.getElementById("btn-unit-next")?.addEventListener("click",()=>{if(g=document.getElementById("unit-select")?.value||"",!g){const e=document.getElementById("unit-error");e&&(e.textContent="Please select a mailbox number.",e.classList.remove("hidden"));return}w(),i("did")});document.getElementById("btn-did-back")?.addEventListener("click",()=>{s?.has_own_ca_address||i("unit")});document.getElementById("btn-did-next")?.addEventListener("click",()=>{if(l=document.querySelector('input[name="did_choice"]:checked')?.value||"",!l){const t=document.getElementById("did-error");t&&(t.textContent="Please select a phone number.",t.classList.remove("hidden"));return}const e=document.getElementById("confirm-unit-row");if(s?.has_own_ca_address)e&&e.classList.add("hidden");else{const t=document.getElementById("confirm-unit");t&&(t.textContent=`${s?.amb_location?.name||"Location"}, Unit ${g}`)}const n=document.getElementById("confirm-did");if(n){const t=l.replace(/\D/g,"");n.textContent=t.length>=10?`+1 (${t.slice(-10,-7)}) ${t.slice(-7,-4)}-${t.slice(-4)}`:l}i("confirm")});document.getElementById("btn-change-unit")?.addEventListener("click",()=>i("unit"));document.getElementById("btn-change-did")?.addEventListener("click",()=>i("did"));document.getElementById("btn-confirm")?.addEventListener("click",async()=>{const e=document.getElementById("btn-confirm"),n=document.getElementById("confirm-status");e.disabled=!0,e.textContent="Confirming...",n&&(n.textContent="Sending your selections...",n.className="mt-3 text-sm text-center text-gray-500",n.classList.remove("hidden"));try{const t=await fetch(`${f}/api/v1/portal/setup-confirm`,{method:"POST",headers:m,body:JSON.stringify({order_id:u,selected_unit:g||null,selected_did:l})}),a=await t.json();if(t.ok&&a.success)i("success");else throw new Error(a.error||"Confirmation failed")}catch(t){n&&(n.textContent=t.message||"Something went wrong.",n.className="mt-3 text-sm text-center text-red-600",n.classList.remove("hidden")),e.disabled=!1,e.innerHTML='<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> Confirm & Begin Setup'}});

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";const u="/api/v1/canada-crtc",t=e=>document.getElementById(e),s=e=>document.getElementById(e),h=new URLSearchParams(window.location.search),m=h.get("order")||"",l=h.get("token")||"";m&&(s("order_number").value=m);l&&sessionStorage.setItem("pw_portal_token",l);const o=l||sessionStorage.getItem("pw_portal_token")||"";function f(){const e={"Content-Type":"application/json"};return o&&(e.Authorization=`Bearer ${o}`),e}if(!o){const e=document.createElement("div");e.className="mt-4 rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-800",e.textContent="This link has expired or is invalid. Please check your email for a new link.",document.querySelector(".max-w-lg")?.appendChild(e)}let c="";t("domain_input")?.addEventListener("input",()=>{const e=s("domain_input");e.value=e.value.toLowerCase().replace(/[^a-z0-9-]/g,"").replace(/^-/,"")});t("btn-search")?.addEventListener("click",async()=>{const e=s("domain_input").value.trim(),r=s("order_number").value.trim();if(!e){alert("Enter a domain name to search.");return}if(!r){alert("Order number is required.");return}const a=e.endsWith(".ca")?e:`${e}.ca`,n=t("btn-search");n.disabled=!0,n.textContent="Searching...",t("result-available")?.classList.add("hidden"),t("result-taken")?.classList.add("hidden"),t("result-error")?.classList.add("hidden"),t("search-result")?.classList.remove("hidden");try{const d=await fetch(`${u}/domain-search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:a,order_number:r})}),i=await d.json();d.ok?i.available?(c=i.domain,t("result-available")?.classList.remove("hidden"),t("result-domain-name").textContent=i.domain):(t("result-taken")?.classList.remove("hidden"),t("result-taken-name").textContent=i.domain):(t("result-error")?.classList.remove("hidden"),t("result-error-text").textContent=i.error||"Search failed.")}catch{t("result-error")?.classList.remove("hidden"),t("result-error-text").textContent="Network error. Please try again."}n.disabled=!1,n.textContent="Search"});t("domain_input")?.addEventListener("keydown",e=>{e.key==="Enter"&&t("btn-search")?.click()});t("btn-confirm")?.addEventListener("click",async()=>{if(!o){alert("Your link has expired. Please check your email for a new link.");return}const e=s("order_number").value.trim();if(!c||!e)return;const r=t("btn-confirm");r.disabled=!0,r.textContent="Registering...";try{const a=await fetch(`${u}/domain-confirm`,{method:"POST",headers:f(),body:JSON.stringify({domain:c,order_number:e})}),n=await a.json();if(a.status===401||a.status===403){alert("Your session has expired. Please check your email for a new link."),r.disabled=!1,r.textContent="Register this domain";return}a.ok&&n.success?(t("search-result")?.classList.add("hidden"),t("confirmed-domain")?.classList.remove("hidden"),t("confirmed-name").textContent=n.domain,s("domain_input").disabled=!0,t("btn-search").disabled=!0):(alert(n.error||"Could not confirm domain. Please try again."),r.disabled=!1,r.textContent="Register this domain")}catch{alert("Network error. Please try again."),r.disabled=!1,r.textContent="Register this domain"}});

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";function t(e){return new URLSearchParams(window.location.search).get(e)??""}const o=t("order_type"),r=t("order_id");if(r){const e=document.getElementById("order-id-display");e&&(e.textContent=`Order ID: ${r}`,e.classList.remove("hidden"))}const d={canada_crtc:"/order/canada-crtc",formation:"/order/formation",bundle:"/order/bundles",compliance:"/services"},a=d[o]??"/",n=document.getElementById("try-again-link");n&&(n.href=a);

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";const e=document.getElementById("about-support-btn");e&&e.addEventListener("click",()=>{const t=document.getElementById("support-fab");t&&t.click()});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";const c=window.__PW_API,l=new URLSearchParams(window.location.search),r=l.get("token");function n(a){["state-loading","state-invalid","state-success","reset-form"].forEach(t=>document.getElementById(t)?.classList.toggle("hidden",t!==a))}n(r?"reset-form":"state-invalid");document.getElementById("reset-form")?.addEventListener("submit",async a=>{a.preventDefault();const t=document.getElementById("new-password").value,i=document.getElementById("confirm-password").value,e=document.getElementById("reset-err"),s=document.getElementById("reset-btn");if(e.classList.add("hidden"),t.length<8){e.textContent="Password must be at least 8 characters.",e.classList.remove("hidden");return}if(t!==i){e.textContent="Passwords don't match.",e.classList.remove("hidden");return}s.disabled=!0,s.textContent="Saving…";try{const o=await fetch(`${c}/api/v1/auth/reset-password`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r,password:t})}),d=await o.json();o.ok?(n("state-success"),d.customer&&window.dispatchEvent(new CustomEvent("pw:auth",{detail:d.customer})),setTimeout(()=>{window.location.href="/"},2500)):(e.textContent=d.error||"Reset failed. Please request a new link.",e.classList.remove("hidden"),s.disabled=!1,s.textContent="Set new password",o.status===400&&n("state-invalid"))}catch{e.textContent="Network error. Please try again.",e.classList.remove("hidden"),s.disabled=!1,s.textContent="Set new password"}});

View file

@ -0,0 +1,22 @@
import"./hoisted.yFz1BYXO.js";const k=window.__PW_API||"https://api.performancewest.net",t=e=>document.getElementById(e),f=t("frn-input"),l=t("btn-check"),b=new URLSearchParams(window.location.search),v=b.get("frn"),g=b.get("entity");v?(f.value=v.replace(/\D/g,"").padStart(10,"0"),setTimeout(()=>m(),500)):g&&fetch(`${k}/api/v1/entities/telecom/${g}`).then(e=>e.json()).then(e=>{e.frn?(f.value=e.frn,setTimeout(()=>m(),300)):h(`Entity "${e.legal_name||g}" has no FRN on file. Add an FRN to run compliance checks.`)}).catch(()=>h("Could not load entity."));l.addEventListener("click",m);f.addEventListener("keydown",e=>{e.key==="Enter"&&m()});const w=t("search-input"),p=t("btn-search");p.addEventListener("click",y);w.addEventListener("keydown",e=>{e.key==="Enter"&&y()});async function y(){const e=w.value.trim();if(!(!e||e.length<2)){p.disabled=!0,p.textContent="Searching...";try{const r=/^\d+$/.test(e),o=new URLSearchParams;r&&e.length===10?o.set("frn",e):r&&e.length<10?o.set("filer_id",e):o.set("q",e);const c=await(await fetch(`${k}/api/v1/fcc/search?${o}`)).json(),x=t("search-results"),n=t("search-list"),a=t("search-count");if(x.classList.remove("hidden"),n.innerHTML="",!c.results||c.results.length===0){a.textContent="No results found. Try a different search or enter the FRN directly below.";return}a.textContent=`${c.count} result${c.count!==1?"s":""} found — click to check:`;for(const s of c.results){const i=document.createElement("button");i.type="button";const u=s.removed_from_rmd?' <span class="text-red-600 font-bold text-xs">REMOVED</span>':"",C=s.source==="rmd"?"RMD":"499 Filer";i.className="w-full text-left px-3 py-2 rounded-lg border border-gray-200 hover:border-pw-400 hover:bg-pw-50 transition-colors text-sm cursor-pointer",i.innerHTML=`
<span class="font-medium text-gray-900">${s.business_name||"Unknown"}${u}</span>
${s.frn?`<span class="text-xs text-gray-500 ml-2">FRN: ${s.frn}</span>`:""}
${s.filer_id?`<span class="text-xs text-gray-500 ml-2">Filer ID: ${s.filer_id}</span>`:""}
<span class="text-xs text-gray-400 ml-2">(${C})</span>
`,i.addEventListener("click",()=>{s.frn?(f.value=s.frn,m()):h("This record has no FRN. Enter an FRN manually to run a compliance check.")}),n.appendChild(i)}}catch{t("search-count").textContent="Search failed. Please try again.",t("search-results")?.classList.remove("hidden")}finally{p.disabled=!1,p.textContent="Search"}}}async function m(){const e=f.value.replace(/\D/g,"").padStart(10,"0");if(e.length!==10||e==="0000000000"){h("Please enter a valid 10-digit FRN.");return}const r=new URL(window.location.href);r.searchParams.set("frn",e),history.replaceState(null,"",r.toString()),t("loading")?.classList.remove("hidden"),t("results")?.classList.add("hidden"),t("error-box")?.classList.add("hidden"),l.disabled=!0,l.textContent="Checking...";try{const o=await fetch(`${k}/api/v1/fcc/lookup?frn=${e}`),d=await o.json();if(!o.ok){h(d.error||"Lookup failed. Please try again.");return}$(d)}catch{h("Could not reach the compliance checker. Please try again.")}finally{t("loading")?.classList.add("hidden"),l.disabled=!1,l.textContent="Check Compliance"}}function h(e){const r=t("error-box"),o=t("error-msg");o.textContent=e,r.classList.remove("hidden"),t("loading")?.classList.add("hidden"),l.disabled=!1,l.textContent="Check Compliance"}function $(e){t("entity-name").textContent=e.entity_name||"Unknown Entity",t("entity-frn").textContent=e.frn;const r=e.cores;r.address?t("entity-address").textContent=[r.address,r.city,r.state,r.zip].filter(Boolean).join(", "):t("entity-address").textContent="Address not available from CORES";const o=t("checks-container");o.innerHTML="";for(const n of e.checks){const a={green:{bg:"bg-green-50",border:"border-green-200",icon:"text-green-600",text:"text-green-800"},yellow:{bg:"bg-amber-50",border:"border-amber-200",icon:"text-amber-600",text:"text-amber-800"},red:{bg:"bg-red-50",border:"border-red-200",icon:"text-red-600",text:"text-red-800"},unknown:{bg:"bg-gray-50",border:"border-gray-200",icon:"text-gray-400",text:"text-gray-600"}},s=a[n.status]||a.unknown,i={green:'<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>',yellow:'<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>',red:'<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>',unknown:'<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"/></svg>'},u=document.createElement("div");u.className=`${s.bg} ${s.border} border rounded-xl p-4 flex items-start gap-3`,u.innerHTML=`
<span class="${s.icon} shrink-0 mt-0.5">${i[n.status]||i.unknown}</span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<p class="text-sm font-semibold ${s.text}">${n.label}</p>
${n.due_date?`<span class="text-xs text-gray-500 shrink-0">Due: ${n.due_date}</span>`:""}
</div>
<p class="text-xs ${s.text} mt-1">${n.detail}</p>
${n.action_url?`<a href="${n.action_url}" target="_blank" class="inline-block text-xs text-pw-600 underline mt-2 hover:text-pw-800">Open portal &rarr;</a>`:""}
</div>
`,o.appendChild(u)}const d=e.checks.filter(n=>n.status==="red"),c=t("catchup-section"),x=t("catchup-items");if(d.length>0){c.classList.remove("hidden"),x.innerHTML="";for(const n of d){const a=document.createElement("div");a.className="flex items-start gap-2",a.innerHTML=`
<svg class="w-4 h-4 text-amber-600 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z"/></svg>
<div>
<p class="text-sm font-medium text-amber-900">${n.label}</p>
<p class="text-xs text-amber-800">${n.detail}</p>
</div>
`,x.appendChild(a)}}else c.classList.add("hidden");t("checked-at").textContent=`Checked at ${new Date(e.checked_at).toLocaleString()}`,t("results")?.classList.remove("hidden")}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import"./hoisted.yFz1BYXO.js";const M=document.getElementById("hero-canvas"),t=M.getContext("2d");let n=0,l=0,w=[],b=[],g=[],S=[],y=0;const P="#0a1628",T="#132744",v="#1e3a5f",k="#0f2744",C="#091a2e";function u(){const a=Math.min(window.devicePixelRatio||1,2);n=M.clientWidth,l=M.clientHeight,M.width=n*a,M.height=l*a,t.setTransform(a,0,0,a,0,0),$()}function $(){const a=l*.55;S=[];for(let i=0;i<(n<640?40:80);i++)S.push({x:Math.random()*n,y:Math.random()*a*.7,r:Math.random()*1.2+.3,twinkle:Math.random()*Math.PI*2,speed:.01+Math.random()*.03});w=[];const e=n<640?4:7;for(let i=0;i<e;i++)w.push({x:Math.random()*n*1.5-n*.25,y:l*.08+Math.random()*a*.35,w:80+Math.random()*160,h:20+Math.random()*30,speed:.08+Math.random()*.15,opacity:.04+Math.random()*.08});b=[];const o=n<640?14:28,h=n/o;for(let i=0;i<o;i++){const c=h*(.5+Math.random()*.4),d=Math.random()<.2?l*.12+Math.random()*l*.18:l*.04+Math.random()*l*.1,f=[],z=Math.floor(d/8);for(let p=0;p<z*3;p++)f.push(Math.random()<.35);const m=12+Math.floor(Math.random()*15);b.push({x:i*h,bw:c,bh:d,lit:f,color:`rgb(${m},${m+8},${m+18})`})}g=[];const r=n<640?2:4;for(let i=0;i<r;i++){const c=n<640?25+Math.random()*20:35+Math.random()*30;g.push({x:n*.1+Math.random()*n*.8,speed:.15+Math.random()*.3,size:c,sailPhase:Math.random()*Math.PI*2,hull:`rgba(${40+Math.floor(Math.random()*30)}, ${50+Math.floor(Math.random()*30)}, ${70+Math.floor(Math.random()*30)}, 0.8)`,sail:`rgba(200, 210, 225, ${.5+Math.random()*.3})`})}}function R(a){const e=l*.55,o=t.createLinearGradient(0,0,0,e);o.addColorStop(0,P),o.addColorStop(.5,T),o.addColorStop(1,v),t.fillStyle=o,t.fillRect(0,0,n,e);const h=n*(.65+Math.sin(a*5e-5)*.1),r=t.createRadialGradient(h,e,0,h,e,n*.4);r.addColorStop(0,"rgba(201, 146, 62, 0.12)"),r.addColorStop(.3,"rgba(201, 146, 62, 0.06)"),r.addColorStop(1,"rgba(201, 146, 62, 0)"),t.fillStyle=r,t.fillRect(0,0,n,e+20)}function G(a){for(const e of S){e.twinkle+=e.speed;const o=.3+Math.sin(e.twinkle)*.3;t.fillStyle=`rgba(200, 215, 240, ${o})`,t.beginPath(),t.arc(e.x,e.y,e.r,0,Math.PI*2),t.fill()}}function I(a){for(const e of w)e.x+=e.speed,e.x>n+e.w&&(e.x=-e.w*1.5),t.fillStyle=`rgba(140, 165, 200, ${e.opacity})`,t.beginPath(),t.ellipse(e.x,e.y,e.w*.5,e.h*.5,0,0,Math.PI*2),t.fill(),t.beginPath(),t.ellipse(e.x-e.w*.25,e.y+e.h*.1,e.w*.35,e.h*.4,0,0,Math.PI*2),t.fill(),t.beginPath(),t.ellipse(e.x+e.w*.2,e.y+e.h*.05,e.w*.3,e.h*.35,0,0,Math.PI*2),t.fill()}function W(a){const e=l*.55;for(const o of b){t.fillStyle=o.color;const h=e-o.bh;t.fillRect(o.x+1,h,o.bw-2,o.bh);const r=2,i=5;let c=0;for(let s=h+4;s<e-3;s+=i)for(let d=o.x+3;d<o.x+o.bw-3;d+=i){if(c<o.lit.length&&o.lit[c]){const f=Math.sin(a*.001+c*7)>.2?1:.3;t.fillStyle=`rgba(255, 220, 140, ${.4*f})`,t.fillRect(d,s,r,r)}c++}}t.strokeStyle="rgba(80, 110, 150, 0.15)",t.lineWidth=1;for(const o of b)t.beginPath(),t.moveTo(o.x+1,e),t.lineTo(o.x+1,e-o.bh),t.lineTo(o.x+o.bw-1,e-o.bh),t.lineTo(o.x+o.bw-1,e),t.stroke()}function q(a){const e=l*.55,o=t.createLinearGradient(0,e,0,l);o.addColorStop(0,k),o.addColorStop(1,C),t.fillStyle=o,t.fillRect(0,e,n,l-e),y+=.3,t.strokeStyle="rgba(100, 150, 200, 0.04)",t.lineWidth=1;for(let i=e+8;i<l;i+=6){t.beginPath();const c=(i-e)/(l-e);t.strokeStyle=`rgba(100, 150, 200, ${.03+c*.02})`;for(let s=0;s<n;s+=3){const d=Math.sin((s+y)*.02+i*.05+a*.001)*(2+c*3);s===0?t.moveTo(s,i+d):t.lineTo(s,i+d)}t.stroke()}const h=n*(.65+Math.sin(a*5e-5)*.1),r=t.createLinearGradient(h-60,e,h+60,l*.8);r.addColorStop(0,"rgba(201, 146, 62, 0.08)"),r.addColorStop(1,"rgba(201, 146, 62, 0)"),t.fillStyle=r,t.fillRect(h-80,e,160,l-e)}function B(a){const e=l*.55;for(const o of g){o.x+=o.speed,o.x>n+o.size*3&&(o.x=-o.size*3),o.sailPhase+=.008;const h=e+20+o.x/n*(l*.2)+Math.sin(o.sailPhase*3)*10,r=Math.sin(a*.002+o.sailPhase)*1.5,i=Math.sin(o.sailPhase)*.05;t.save(),t.translate(o.x,h+r),t.rotate(i),t.fillStyle=o.hull,t.beginPath(),t.moveTo(-o.size,0),t.quadraticCurveTo(-o.size*.8,o.size*.4,o.size*.3,o.size*.3),t.lineTo(o.size,0),t.closePath(),t.fill(),t.strokeStyle="rgba(180, 190, 200, 0.6)",t.lineWidth=1,t.beginPath(),t.moveTo(0,0),t.lineTo(0,-o.size*1.8),t.stroke();const c=Math.sin(o.sailPhase)*o.size*.15;t.fillStyle=o.sail,t.beginPath(),t.moveTo(0,-o.size*1.7),t.quadraticCurveTo(o.size*.5+c,-o.size*.8,0,-o.size*.1),t.closePath(),t.fill(),t.fillStyle=`rgba(200, 210, 225, ${.3+Math.sin(o.sailPhase+1)*.1})`,t.beginPath(),t.moveTo(0,-o.size*1.5),t.quadraticCurveTo(-o.size*.4-c*.5,-o.size*.7,-o.size*.1,-o.size*.1),t.closePath(),t.fill(),t.restore(),t.strokeStyle="rgba(150, 180, 210, 0.08)",t.lineWidth=1,t.beginPath();for(let s=0;s<o.size*2;s+=3){const d=o.x-o.size-s,f=h+r+Math.sin(s*.2+a*.003)*1;s===0?t.moveTo(d,f):t.lineTo(d,f)}t.stroke()}}function x(a){t.clearRect(0,0,n,l),R(a),G(),I(),W(a),q(a),B(a),requestAnimationFrame(x)}function O(){u(),window.addEventListener("resize",()=>{u()}),requestAnimationFrame(x)}O();

View file

@ -0,0 +1,30 @@
import"./hoisted.yFz1BYXO.js";const c=[{id:1,text:"Does your privacy policy list all categories of personal information you collect?",detail:"CCPA requires disclosure of categories such as identifiers, commercial information, internet activity, geolocation data, biometric data, and more.",gap:"Your privacy policy should enumerate every category of personal information you collect, not just a general statement."},{id:2,text:"Does it disclose who you share data with?",detail:"You must identify the categories of third parties that receive personal information (e.g., service providers, analytics vendors, advertising partners).",gap:"Add a section listing all categories of third parties you share personal data with and the purpose for each."},{id:3,text:"Does it include a California-specific section (CCPA)?",detail:"If you meet CCPA thresholds, your policy must include California-specific disclosures about consumer rights.",gap:"Add a dedicated CCPA section covering California consumers' right to know, delete, opt-out, and non-discrimination."},{id:4,text:'Do you have a working "Do Not Sell My Personal Information" opt-out link?',detail:"CCPA requires a clear, conspicuous link on your homepage if you sell or share personal information.",gap:'Add a prominent "Do Not Sell or Share My Personal Information" link to your website footer and homepage.'},{id:5,text:"Do you respond to consumer data requests within 45 days?",detail:"CCPA requires verifiable consumer requests to be acknowledged within 10 days and fulfilled within 45 days.",gap:"Establish an internal process to receive, verify, and respond to consumer data access and deletion requests within the 45-day window."},{id:6,text:"Is your privacy policy updated within the last 12 months?",detail:"Privacy policies must be reviewed and updated at least annually to reflect current practices.",gap:"Review and update your privacy policy immediately. Add a 'Last Updated' date and set a recurring annual review."},{id:7,text:"Do you have a cookie consent mechanism?",detail:"Many jurisdictions and industry standards require informing users about cookies and obtaining consent before setting non-essential cookies.",gap:"Implement a cookie consent banner that allows users to accept or reject non-essential cookies before they are set."},{id:8,text:"Does your privacy policy include your physical mailing address?",detail:"California law and other regulations require a physical address where consumers can send written inquiries.",gap:"Add your company's physical mailing address to your privacy policy contact section."}];function p(){const t=document.getElementById("checklist-container");t&&(t.innerHTML=c.map(e=>`
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<div class="flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-pw-100 text-pw-700 text-sm font-bold flex items-center justify-center">${e.id}</span>
<div class="flex-1">
<p class="font-medium text-gray-900 mb-1">${e.text}</p>
<p class="text-xs text-gray-500 mb-4">${e.detail}</p>
<div class="flex gap-3">
<label class="flex-1 cursor-pointer">
<input type="radio" name="c${e.id}" value="yes" class="peer sr-only" required />
<div class="peer-checked:bg-green-50 peer-checked:border-green-300 peer-checked:text-green-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
Yes
</div>
</label>
<label class="flex-1 cursor-pointer">
<input type="radio" name="c${e.id}" value="no" class="peer sr-only" required />
<div class="peer-checked:bg-red-50 peer-checked:border-red-300 peer-checked:text-red-700 border border-gray-200 rounded-lg py-2 px-4 text-center text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors">
No
</div>
</label>
</div>
</div>
</div>
</div>
`).join(""))}function y(){let t=0;const e=[];return c.forEach(o=>{const s=document.querySelector(`input[name="c${o.id}"]:checked`);s&&s.value==="no"&&(t++,e.push({text:o.text,gap:o.gap}))}),{noCount:t,gaps:e}}function g(t,e){const o=document.getElementById("results"),s=document.getElementById("result-card"),i=document.getElementById("result-icon"),n=document.getElementById("result-title"),r=document.getElementById("result-score"),a=document.getElementById("result-description"),l=document.getElementById("result-gaps"),u=document.getElementById("checklist-form");if(!o||!s||!i||!n||!r||!a||!l||!u)return;u.classList.add("hidden"),o.classList.remove("hidden");const d=8-t;t===0?(s.className="rounded-xl p-8 border-2 text-center bg-green-50 border-green-300",i.textContent="✅",n.textContent="Good Standing",n.className="text-2xl font-bold mb-3 text-green-800",r.textContent=`${d}/8 items passed. No gaps identified.`,r.className="text-sm font-medium mb-4 text-green-700",a.textContent="Your privacy policy appears to cover all the key elements we checked. Keep it up-to-date with annual reviews, especially as privacy laws evolve. Consider a professional review to catch nuances this checklist cannot."):t<=3?(s.className="rounded-xl p-8 border-2 text-center bg-amber-50 border-amber-300",i.textContent="⚠️",n.textContent="Needs Attention",n.className="text-2xl font-bold mb-3 text-amber-800",r.textContent=`${d}/8 items passed. ${t} gap${t>1?"s":""} identified.`,r.className="text-sm font-medium mb-4 text-amber-700",a.textContent="Your privacy policy has some gaps that could expose you to regulatory enforcement or consumer complaints. These are addressable issues, but you should prioritize resolving them."):(s.className="rounded-xl p-8 border-2 text-center bg-red-50 border-red-300",i.textContent="🚨",n.textContent="Significant Gaps",n.className="text-2xl font-bold mb-3 text-red-800",r.textContent=`${d}/8 items passed. ${t} gaps identified.`,r.className="text-sm font-medium mb-4 text-red-700",a.textContent="Your privacy policy has significant deficiencies that could result in regulatory fines, consumer lawsuits, or enforcement actions. Under CCPA, penalties can reach $2,500 per unintentional violation and $7,500 per intentional violation. A professional review is strongly recommended."),e.length>0&&(l.innerHTML='<p class="font-medium text-gray-700 mb-3">Identified gaps and recommended actions:</p>'+e.map(m=>`
<div class="bg-white border border-gray-200 rounded-lg p-4">
<p class="font-medium text-gray-800 text-sm mb-1">${m.text}</p>
<p class="text-xs text-gray-600">${m.gap}</p>
</div>
`).join("")),o.scrollIntoView({behavior:"smooth",block:"start"})}document.addEventListener("DOMContentLoaded",()=>{p(),document.getElementById("checklist-form")?.addEventListener("submit",e=>{e.preventDefault();let o=!0;if(c.forEach(n=>{document.querySelector(`input[name="c${n.id}"]:checked`)||(o=!1)}),!o){alert("Please answer all 8 questions before submitting.");return}const{noCount:s,gaps:i}=y();g(s,i)}),document.getElementById("retake-btn")?.addEventListener("click",()=>{const e=document.getElementById("checklist-form"),o=document.getElementById("results");e&&o&&(e.classList.remove("hidden"),o.classList.add("hidden"),p(),window.scrollTo({top:0,behavior:"smooth"}))})});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
site/public/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#2d4e78"/>
<text x="16" y="22.5" font-family="'Inter','Segoe UI',system-ui,sans-serif" font-weight="800" font-size="15" fill="white" text-anchor="middle" letter-spacing="-1">PW</text>
</svg>

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471"><rect width="750" height="471" rx="40" fill="#016FD0"/><path fill="#fff" d="M83 195l-42 108h50l6-16h14l6 16h55v-12l5 12h28l5-12v12h114v-23h-3c-2 0-3-1-3-3v-82h3c2 0 3-1 3-3v-23H297l-5 12-5-12H166v12l-5-12H111zm11 15h25l28 65v-65h27l22 37 20-37h27v79h-17v-62l-24 44h-15l-24-44v62h-33l-6-16H99l-7 16H75zm24 21l-10 28h21zm254-21h-56v79h56v-17h-38v-14h37v-16h-37v-15h38zm35 0h-20v79h20v-30l8-8 24 38h24l-34-50 31-29h-24l-29 31zm62 0v79h18v-27h19c18 0 31-10 31-26s-13-26-31-26h-37zm18 16h17c9 0 14 5 14 10s-5 10-14 10h-17zm91-16h-21v79h17v-28h5c6 0 9 2 10 8l3 20h18l-4-23c-1-8-5-12-11-14 7-3 12-10 12-19 0-15-11-23-29-23zm-3 16h14c9 0 14 4 14 10 0 7-5 11-13 11h-15zm60 63v-79h55v17h-37v15h36v16h-36v14h37v17zm56-79h26l15 26 15-26h25v79h-17v-58l-18 29h-14l-18-29v58h-14zm104 27c0-17 13-29 33-29 9 0 17 2 23 6v19c-6-6-13-9-22-9-9 0-16 5-16 13s7 13 16 13c9 0 16-3 22-9v19c-6 4-14 6-23 6-20 0-33-12-33-29z" transform="translate(-30,28) scale(0.94)"/></svg>

After

Width:  |  Height:  |  Size: 1,010 B

View file

@ -0,0 +1 @@
<svg fill="#F7931A" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bitcoin</title><path d="M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z"/></svg>

After

Width:  |  Height:  |  Size: 923 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471"><rect width="750" height="471" rx="40" fill="#fff"/><rect x="2" y="2" width="746" height="467" rx="38" fill="none" stroke="#e6e6e6" stroke-width="3"/><path fill="#F47216" opacity="0.12" d="M375 2h335c21 0 38 17 38 38v195H375z"/><circle cx="465" cy="236" r="80" fill="#F47216"/><path fill="#F47216" d="M375 305h375v126c0 21-17 38-38 38H375z" opacity="0.15"/><g fill="#231F20" transform="translate(55,195)"><path d="M0 4h26c35 0 56 20 56 50 0 31-21 52-56 52H0V4zm15 88h9c27 0 42-14 42-38 0-23-15-36-42-36h-9v74z"/><path d="M100 106V4h15v102H100z"/><path d="M141 68c1 11 10 19 22 19 10 0 18-4 18-12 0-7-5-11-16-14l-12-3c-17-5-25-13-25-26 0-17 14-29 34-29 22 0 36 11 36 28h-15c-1-10-8-16-21-16-12 0-19 6-19 13 0 7 5 11 16 13l11 3c18 5 27 13 27 27 0 19-15 30-36 30-22 0-36-11-37-29h16z"/><path d="M225 55c0 30 21 53 51 53 13 0 24-4 33-11V75c-9 9-20 14-33 14-22 0-36-16-36-34 0-21 15-37 36-37 13 0 24 5 33 15V11c-9-7-20-11-33-11-30 0-51 22-51 55z"/><path d="M403 4l34 80L470 4h17L444 106h-14L388 4h15z"/><path d="M498 4v102h-15V4h15z"/><path d="M523 4h56v14h-41v28h39v14h-39v32h41v14h-56V4z"/><path d="M600 4h35c22 0 36 12 36 30 0 14-8 24-22 28l27 44h-18l-24-41h-19v41h-15V4zm15 48h18c14 0 22-7 22-18s-8-17-22-17h-18v35z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg fill="#3C3C3D" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ethereum</title><path d="M11.944 17.97L4.58 13.62 11.943 24l7.37-10.38-7.372 4.35h.003zM12.056 0L4.69 12.223l7.365 4.354 7.365-4.35L12.056 0z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

View file

@ -0,0 +1 @@
<svg fill="#345D9D" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Litecoin</title><path d="M12 0a12 12 0 1012 12A12 12 0 0012 0zm-.2617 3.6777h2.584a.3425.3425 0 01.33.4356l-2.0312 6.918 1.9062-.582-.4082 1.3847-1.9238.5605-1.248 4.213h6.6757a.3425.3425 0 01.3282.4374l-.582 2a.4586.4586 0 01-.4395.3301H6.7324l1.7227-5.8223-1.9063.5801.42-1.3613 1.9101-.58 2.4219-8.1798a.4557.4557 0 01.4375-.334Z"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471"><rect width="750" height="471" rx="40" fill="#fff"/><rect x="2" y="2" width="746" height="467" rx="38" fill="none" stroke="#e6e6e6" stroke-width="3"/><circle cx="301" cy="236" r="118" fill="#EB001B"/><circle cx="449" cy="236" r="118" fill="#F79E1B"/><path fill="#FF5F00" d="M375 141c-29 23-48 59-48 99s19 75 48 98c29-23 48-59 48-98 0-40-19-76-48-99z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 124 33" fill="none">
<path fill="#253B80" d="M46.211 6.749h-6.839a.95.95 0 0 0-.939.802l-2.766 17.537a.57.57 0 0 0 .564.658h3.265a.95.95 0 0 0 .939-.803l.746-4.73a.95.95 0 0 1 .938-.803h2.165c4.505 0 7.105-2.18 7.784-6.5.306-1.89.013-3.375-.872-4.415-.972-1.142-2.696-1.746-4.985-1.746zM47.062 13.154c-.374 2.454-2.249 2.454-4.062 2.454h-1.032l.724-4.583a.57.57 0 0 1 .563-.481h.473c1.235 0 2.4 0 3.002.704.359.42.469 1.044.332 1.906zM66.654 13.075h-3.275a.57.57 0 0 0-.563.481l-.145.916-.228-.332c-.709-1.029-2.29-1.374-3.868-1.374-3.619 0-6.71 2.741-7.312 6.586-.313 1.918.132 3.752 1.22 5.031.998 1.176 2.426 1.666 4.125 1.666 2.916 0 4.533-1.875 4.533-1.875l-.146.91a.57.57 0 0 0 .562.66h2.95a.95.95 0 0 0 .939-.804l1.77-11.209a.568.568 0 0 0-.562-.656zm-4.565 6.374c-.316 1.871-1.801 3.127-3.695 3.127-.951 0-1.711-.305-2.199-.883-.484-.574-.668-1.391-.514-2.301.295-1.855 1.805-3.152 3.67-3.152.93 0 1.686.309 2.184.892.499.589.697 1.411.554 2.317zM84.096 13.075h-3.291a.954.954 0 0 0-.787.417l-4.539 6.686-1.924-6.425a.953.953 0 0 0-.912-.678h-3.234a.57.57 0 0 0-.541.754l3.625 10.638-3.408 4.811a.57.57 0 0 0 .465.9h3.287a.949.949 0 0 0 .781-.408l10.946-15.8a.57.57 0 0 0-.468-.895z"/>
<path fill="#179BD7" d="M94.992 6.749h-6.84a.95.95 0 0 0-.938.802l-2.766 17.537a.569.569 0 0 0 .562.658h3.51a.665.665 0 0 0 .656-.562l.785-4.971a.95.95 0 0 1 .938-.803h2.164c4.506 0 7.105-2.18 7.785-6.5.307-1.89.012-3.375-.873-4.415-.971-1.142-2.694-1.746-4.983-1.746zm.851 6.405c-.373 2.454-2.248 2.454-4.062 2.454h-1.031l.724-4.583a.567.567 0 0 1 .562-.481h.473c1.234 0 2.4 0 3.002.704.359.42.468 1.044.332 1.906zM115.434 13.075h-3.273a.567.567 0 0 0-.562.481l-.145.916-.23-.332c-.709-1.029-2.289-1.374-3.867-1.374-3.619 0-6.709 2.741-7.311 6.586-.312 1.918.131 3.752 1.219 5.031 1 1.176 2.426 1.666 4.125 1.666 2.916 0 4.533-1.875 4.533-1.875l-.146.91a.57.57 0 0 0 .564.66h2.949a.95.95 0 0 0 .938-.804l1.771-11.209a.571.571 0 0 0-.565-.656zm-4.565 6.374c-.314 1.871-1.801 3.127-3.695 3.127-.949 0-1.711-.305-2.199-.883-.484-.574-.666-1.391-.514-2.301.297-1.855 1.805-3.152 3.67-3.152.93 0 1.686.309 2.184.892.501.589.699 1.411.554 2.317zM119.295 7.23l-2.807 17.858a.569.569 0 0 0 .562.658h2.822a.949.949 0 0 0 .939-.803l2.768-17.536a.57.57 0 0 0-.562-.658h-3.16a.571.571 0 0 0-.562.481z"/>
<path fill="#253B80" d="M7.266 29.154l.523-3.322-1.165-.027H1.061L4.927 1.292a.316.316 0 0 1 .314-.268h9.38c3.114 0 5.263.648 6.385 1.927.526.6.861 1.227 1.023 1.917.17.724.173 1.589.007 2.644l-.012.077v.676l.526.298a3.69 3.69 0 0 1 1.065.812c.45.513.741 1.165.864 1.938.127.795.085 1.741-.123 2.812-.24 1.232-.628 2.305-1.152 3.183a6.547 6.547 0 0 1-1.825 2c-.696.494-1.523.869-2.458 1.109-.906.236-1.939.355-3.072.355h-.73a2.21 2.21 0 0 0-2.183 1.866l-.055.299-.924 5.855-.042.215c-.011.068-.03.102-.058.125a.155.155 0 0 1-.096.035H7.266z"/>
<path fill="#179BD7" d="M23.048 7.667c-.028.179-.06.362-.096.55-1.237 6.351-5.469 8.545-10.874 8.545H9.326c-.661 0-1.218.48-1.321 1.132l-1.41 8.938-.399 2.533a.704.704 0 0 0 .695.814h4.881c.578 0 1.069-.42 1.159-.99l.048-.248.919-5.832.059-.32c.09-.572.582-.992 1.16-.992h.73c4.729 0 8.431-1.92 9.513-7.476.452-2.321.218-4.259-.978-5.622a4.667 4.667 0 0 0-1.336-1.032z"/>
<path fill="#222D65" d="M21.754 7.151a9.757 9.757 0 0 0-1.203-.267 15.284 15.284 0 0 0-2.426-.177H11.22a1.16 1.16 0 0 0-1.159.992L8.704 14.28l-.055.341a1.357 1.357 0 0 1 1.321-1.132h2.752c5.405 0 9.637-2.195 10.874-8.545.037-.188.068-.371.096-.55a6.594 6.594 0 0 0-1.938-.743z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1 @@
<svg fill="#2775CA" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Circle</title><path d="M20.788 3.832c-.101-.105-.197-.213-.301-.317-.103-.103-.211-.202-.32-.302A11.903 11.903 0 0 0 12 0a11.926 11.926 0 0 0-8.486 3.514C-1.062 8.09-1.16 15.47 3.213 20.168c.099.108.197.214.3.32.104.103.21.2.317.3A11.92 11.92 0 0 0 12 24c3.206 0 6.22-1.247 8.487-3.512 4.576-4.576 4.673-11.956.301-16.656zm-16.655.301A11.057 11.057 0 0 1 12 .874c2.825 0 5.49 1.048 7.55 2.958l-1.001 1.002A9.646 9.646 0 0 0 12 2.292a9.644 9.644 0 0 0-6.865 2.844A9.644 9.644 0 0 0 2.292 12c0 2.448.9 4.753 2.542 6.549L3.831 19.55C-.201 15.191-.101 8.367 4.133 4.133zm13.798 1.318v.002l-1.015 1.014A7.346 7.346 0 0 0 12 4.589 7.357 7.357 0 0 0 6.761 6.76 7.362 7.362 0 0 0 4.589 12a7.34 7.34 0 0 0 1.877 4.913l-1.014 1.016A8.77 8.77 0 0 1 3.167 12a8.77 8.77 0 0 1 2.588-6.245A8.771 8.771 0 0 1 12 3.167c2.213 0 4.301.809 5.931 2.284zM18.537 12c0 1.745-.681 3.387-1.916 4.622S13.746 18.538 12 18.538a6.491 6.491 0 0 1-4.296-1.621l-.001-.004c-.11-.094-.22-.188-.324-.291a6.027 6.027 0 0 1-.293-.326A6.47 6.47 0 0 1 5.466 12c0-1.746.679-3.387 1.914-4.621A6.488 6.488 0 0 1 12 5.465c1.599 0 3.105.576 4.295 1.62.111.096.224.19.326.295.104.104.2.214.295.324A6.482 6.482 0 0 1 18.537 12zM7.084 17.534h.001A7.349 7.349 0 0 0 12 19.413a7.35 7.35 0 0 0 5.239-2.174A7.354 7.354 0 0 0 19.412 12a7.364 7.364 0 0 0-1.876-4.916l1.013-1.012A8.777 8.777 0 0 1 20.834 12a8.765 8.765 0 0 1-2.589 6.246A8.764 8.764 0 0 1 12 20.834a8.782 8.782 0 0 1-5.93-2.285l1.014-1.015zm12.783 2.333A11.046 11.046 0 0 1 12 23.125a11.042 11.042 0 0 1-7.551-2.957l1.004-1.001a9.64 9.64 0 0 0 6.549 2.542 9.639 9.639 0 0 0 6.865-2.846A9.642 9.642 0 0 0 21.71 12a9.64 9.64 0 0 0-2.543-6.548l1.001-1.002c4.031 4.359 3.935 11.182-.301 15.417z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg fill="#50AF95" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Tether</title><path d="M18.7538 10.5176c0 .6251-2.2379 1.1483-5.2381 1.2812l.0028.0007c-.0848.0064-.5233.0325-1.5012.0325-.7778 0-1.33-.0233-1.5237-.0325-3.0059-.1322-5.2495-.6555-5.2495-1.2819s2.2436-1.149 5.2495-1.2834v2.0442c.1965.0142.7594.0474 1.5372.0474.9334 0 1.4008-.0389 1.4849-.0466V9.2356c2.9994.1337 5.2381.657 5.2381 1.282zm5.19.5466L12.1248 22.389a.1803.1803 0 0 1-.2496 0L.0562 11.0635a.1781.1781 0 0 1-.0382-.2079l4.3762-9.1921a.1767.1767 0 0 1 .1626-.1026h14.8878a.1768.1768 0 0 1 .1612.1032l4.3762 9.1922a.1782.1782 0 0 1-.0382.2079zm-4.478-.4038c0-.8068-2.5515-1.4799-5.9473-1.6369V7.195h4.186V4.4055H6.3076V7.195h4.1852v1.8286c-3.4018.1562-5.9601.83-5.9601 1.6376 0 .8075 2.5583 1.4806 5.9601 1.6376v5.8618h3.025v-5.8639c3.394-.1563 5.948-.8295 5.948-1.6363z"/></svg>

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471"><rect width="750" height="471" rx="40" fill="#fff"/><rect x="2" y="2" width="746" height="467" rx="38" fill="none" stroke="#e6e6e6" stroke-width="3"/><path fill="#1A1F71" d="M278 334l33-196h53l-33 196zm222-191c-11-4-27-8-48-8-53 0-90 27-90 65 0 28 26 44 47 53 21 10 28 16 28 24 0 13-17 19-32 19-21 0-33-3-50-10l-7-3-8 44c13 5 36 10 60 10 56 0 92-26 93-69 0-23-14-40-46-55-19-9-31-15-31-25 0-8 10-17 31-17 18 0 31 4 41 8l5 2 7-43zm134-5h-41c-13 0-22 4-28 16l-79 180h56s9-24 11-30h69c2 7 6 30 6 30h50l-44-196zm-66 126c4-11 21-55 21-55l7-19 4 17s10 47 12 57h-44zM287 153l-52 134-6-27c-10-31-40-65-74-82l48 171h57l84-196h-57z"/><path fill="#F9A533" d="M147 153H61l-1 4c67 16 112 55 130 103l-19-90c-3-12-13-16-24-17z"/></svg>

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

BIN
site/public/images/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

86
site/public/index.html Normal file

File diff suppressed because one or more lines are too long

114
site/public/llms.txt Normal file
View file

@ -0,0 +1,114 @@
# Performance West Inc.
> Business formation and compliance consulting in all 50 US states.
> Fixed pricing. Professional results. We do not provide legal advice.
## Company
Performance West Inc. is a compliance consulting firm based in Cheyenne, Wyoming.
We offer automated business formation in all 50 states plus DC, and compliance
consulting services covering employment law, data privacy, TCPA, and telecom.
- Website: https://performancewest.net
- Phone: 1-888-411-0383
- Email: info@performancewest.net
- Address: 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001
## Business Formation (All 50 States + DC)
Form an LLC, Corporation, or S-Corp in any US state.
- Basic tier: $149 + state filing fees (formation filing only)
- Complete tier: $399 + state filing fees (includes EIN + Operating Agreement + 1st year Registered Agent)
- Name availability search: Free
- Registered Agent: $125/year per state
- EIN obtainment: $49 (standalone)
- Operating Agreement: $99 (standalone)
- Annual Report Filing: $99/state/year
- State Registration / Foreign Qualification: $249/state
- Expedited processing available (varies by state)
- Turnaround: 1-7 business days depending on state
Order: https://performancewest.net/order/formation
Formation guide: https://performancewest.net/tools/formation-guide
## Compliance Consulting Services
### Employment Compliance
- FLSA / Wage & Hour Audit: $1,499 (up to 50 employees), 5-7 days
- Contractor Classification Review: $499/worker, 3-5 days
- Employee Handbook Review: $999, 5-7 days
- Workplace Policy Development: Custom quote, 2-3 weeks
### Data Privacy
- CCPA/CPRA Compliance Audit: $2,499, 7-10 days
- Privacy Policy Generation & Review: $499, 5-7 days
- Data Mapping & Inventory: Custom quote, 1-3 weeks
- Breach Response Planning: $1,999, 2-3 weeks
### TCPA Compliance
- SMS/Call Consent Audit: $1,299, 5-7 days
- DNC List Compliance Review: $799, 3-5 days
- Marketing Campaign Compliance Review: $599, 2-3 days
### Telecom Compliance
- FCC Form 499-A Filing: $799, 5-7 days
- STIR/SHAKEN Implementation: Custom quote, 2-4 weeks
- IPES & ISP Registrations: $1,299, 2-3 weeks
- Telecom Database Management: $499/quarter
- State PUC/PSC Filings: $399/state, 2-8 weeks
### Attorney Review Add-On
Any compliance service can include independent attorney review ($399-$699).
A licensed attorney reviews our deliverable and provides a legal opinion letter.
Details: https://performancewest.net/services/legal-review
## Free Tools
- Formation Guide (what state + entity type): https://performancewest.net/tools/formation-guide
- Contractor Classification Quiz: https://performancewest.net/tools/contractor-quiz
- Privacy Policy Compliance Check: https://performancewest.net/tools/privacy-check
- TCPA SMS Compliance Check: https://performancewest.net/tools/tcpa-check
## For AI Agents
### MCP Server
Install: npx @performancewest/mcp-server
GitHub: https://github.com/performancewest/mcp-server
Tools available:
- formation_guide — Recommend entity type + state based on user's situation
- search_name — Check business name availability in a state
- create_formation_order — Place a formation order ($149 basic / $399 complete)
- check_order_status — Track order by number
- get_states — All 51 jurisdictions with fees
- get_state_info — Detailed state info (fees, taxes, requirements)
- get_pricing — Calculate total formation cost with add-ons and discounts
- list_services — All 20+ compliance services with pricing
- get_service_info — Service details by slug
- validate_discount — Check discount/referral codes
### API
Base URL: https://api.performancewest.net
Endpoints:
- GET /api/v1/states — All states with fees
- GET /api/v1/states/:code/name-search?name=... — Name search
- POST /api/v1/formations — Create formation order
- GET /api/v1/formations/:orderNumber — Check status
- GET /api/v1/discount/:code — Validate discount code
- POST /api/v1/subscribe — Mailing list signup
- POST /api/v1/tickets — Support ticket
- POST /api/v1/quotes — Quote request
### Partner Program
API keys available for AI platforms and professional service providers.
Commission on orders placed through your integration.
Contact: partners@performancewest.net
## Important Disclaimers
Performance West Inc. provides compliance consulting services.
We do NOT provide legal advice, legal representation, or create attorney-client relationships.
For legal matters, consult a licensed attorney in your jurisdiction.
Attorney review is available as an add-on through our network of independent, licensed attorneys.
The attorney-client relationship is between the customer and the reviewing attorney.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
<!DOCTYPE html><html lang="en" data-astro-cid-bhlkm3xi> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Identity Verification — Performance West Inc.</title><meta name="robots" content="noindex, nofollow"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="stylesheet" href="/_astro/about.DhmoKVOS.css">
<style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f9fafb}.card[data-astro-cid-bhlkm3xi]{background:#fff;border-radius:1rem;padding:2.5rem;max-width:28rem;width:100%;margin:1rem;text-align:center;box-shadow:0 1px 3px #0000001a}.spinner[data-astro-cid-bhlkm3xi]{width:2.5rem;height:2.5rem;border:3px solid #e5e7eb;border-top-color:#1e40af;border-radius:50%;animation:spin .8s linear infinite;margin:0 auto 1.5rem}@keyframes spin{to{transform:rotate(360deg)}}h1[data-astro-cid-bhlkm3xi]{font-size:1.125rem;font-weight:600;color:#111827;margin:0 0 .5rem}p[data-astro-cid-bhlkm3xi]{font-size:.875rem;color:#6b7280;margin:0}
</style><script type="module">(function(){const e=new URLSearchParams(window.location.search),s=e.get("session_id")||e.get("VERIFICATION_SESSION_ID");s&&sessionStorage.setItem("pw_identity_session",s);const n=sessionStorage.getItem("pw_order_return_url")||"/order/canada-crtc";window.location.replace(n+(n.includes("?")?"&":"?")+"identity_return=1")})();
</script></head> <body data-astro-cid-bhlkm3xi> <div class="card" data-astro-cid-bhlkm3xi> <div class="spinner" data-astro-cid-bhlkm3xi></div> <h1 data-astro-cid-bhlkm3xi>Returning to your order...</h1> <p data-astro-cid-bhlkm3xi>Processing your verification result. Please wait.</p> </div> </body> </html>

View file

@ -0,0 +1,310 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Obtain a NECA Operating Company Number (OCN) for your telecom carrier. Required for STIR/SHAKEN, LRN, and numbering authority. $2,650 all-in.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>NECA OCN Registration — Performance West Inc.</title>
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f9fafb;line-height:1.6}
a{color:#1e3a5f;text-decoration:none}
.wrap{max-width:680px;margin:0 auto;padding:2rem 1.25rem 4rem}
h1{font-size:1.65rem;font-weight:700;color:#111827;margin-bottom:.25rem}
.subtitle{font-size:.9rem;color:#6b7280;margin-bottom:1.5rem}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1.25rem;margin-bottom:1rem}
.card h2{font-size:1.05rem;font-weight:700;color:#1e3a5f;margin-bottom:.75rem}
.label{display:block;font-size:.82rem;font-weight:600;color:#374151;margin-bottom:.3rem;margin-top:.6rem}
select,input[type=text],input[type=email],input[type=tel]{width:100%;padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:8px;font-size:.88rem;background:#fff}
select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px rgba(30,58,95,.15)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;font-size:.92rem;cursor:pointer;border:none;transition:all .15s}
.btn-primary{background:#1e3a5f;color:#fff}.btn-primary:hover{background:#162e4d}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.info-banner{padding:.6rem .9rem;background:#ecfdf5;border-left:3px solid #059669;border-radius:0 6px 6px 0;font-size:.85rem;color:#065f46;margin-bottom:1.25rem}
.price-box{background:#f0f4f8;border:2px solid #1e3a5f;border-radius:10px;padding:1rem;text-align:center;margin-bottom:1.25rem}
.price-box .price{font-size:1.8rem;font-weight:800;color:#1e3a5f}
.price-box .price-note{font-size:.78rem;color:#6b7280}
.feature-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin:.75rem 0}
.feature-item{display:flex;align-items:flex-start;gap:.4rem;font-size:.82rem;color:#374151}
.feature-item svg{width:16px;height:16px;color:#059669;flex-shrink:0;margin-top:2px}
.state-picker{display:grid;grid-template-columns:repeat(auto-fill, minmax(110px, 1fr));gap:.35rem;max-height:260px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px;padding:.5rem}
.state-chip{display:flex;align-items:center;gap:.3rem;padding:.25rem .5rem;border-radius:6px;font-size:.78rem;cursor:pointer;border:1px solid transparent;transition:all .1s}
.state-chip:hover{background:#f0f4f8}
.state-chip.selected{background:#eff6ff;border-color:#1e3a5f;font-weight:600}
.state-chip input{accent-color:#1e3a5f;width:14px;height:14px}
.tandem-box{background:#fffbeb;border:1px solid #fbbf24;border-radius:8px;padding:.75rem;margin-top:.75rem}
.tandem-box h3{font-size:.88rem;font-weight:700;color:#92400e;margin-bottom:.3rem}
.hidden{display:none}
.err{color:#dc2626;font-size:.82rem;margin-top:.5rem;display:none}
</style>
</head>
<body>
<div class="wrap">
<h1>NECA OCN Registration</h1>
<p class="subtitle">Obtain a unique Operating Company Number from NECA. Required for STIR/SHAKEN signing, LRN assignments, and numbering authority.</p>
<div class="price-box">
<div class="price">$2,650</div>
<div class="price-note">All-in: $650 OCN application + $2,000 sponsoring CLEC agreement</div>
</div>
<div class="info-banner">
<strong>Tax deductible</strong> &mdash; OCN registration fees are deductible as ordinary business expenses under IRC &sect; 162.
</div>
<!-- What's included -->
<div class="card">
<h2>What's Included</h2>
<div class="feature-grid">
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>NECA Company Code application</span>
</div>
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>Sponsoring CLEC agreement</span>
</div>
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>STIR/SHAKEN readiness</span>
</div>
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>LRN / number portability</span>
</div>
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>10 business day processing</span>
</div>
<div class="feature-item">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span>NECA filing fee included</span>
</div>
</div>
</div>
<!-- Intake Form -->
<div class="card">
<h2>Carrier Information</h2>
<label class="label">Legal Entity Name *</label>
<input type="text" id="entity-name" placeholder="Acme Telecom LLC">
<label class="label">Contact Name *</label>
<input type="text" id="contact-name" placeholder="John Smith">
<label class="label">Contact Email *</label>
<input type="email" id="contact-email" placeholder="john@acmetelecom.com">
<label class="label">Contact Phone *</label>
<input type="tel" id="contact-phone" placeholder="(555) 123-4567">
<label class="label">FRN (FCC Registration Number)</label>
<input type="text" id="frn" placeholder="0012345678" maxlength="10">
<label class="label">Service Category *</label>
<select id="service-category">
<option value="IPES">IPES (Interconnected VoIP)</option>
<option value="CLEC">CLEC (Competitive Local Exchange Carrier)</option>
<option value="Non-Incumbent LEC">Non-Incumbent LEC</option>
<option value="ISP">ISP (Internet Service Provider)</option>
<option value="Reseller">Reseller</option>
<option value="Other">Other</option>
</select>
<label class="label">Operating States * <span style="font-weight:400;color:#9ca3af">(select all that apply)</span></label>
<div style="margin-bottom:.3rem">
<label style="font-size:.78rem;cursor:pointer">
<input type="checkbox" id="select-all-states" style="accent-color:#1e3a5f"> Select all states
</label>
</div>
<div class="state-picker" id="state-picker"></div>
<div class="tandem-box" id="tandem-box">
<h3>Sponsoring CLEC / Tandem Agreement</h3>
<p style="font-size:.82rem;color:#78350f;margin-bottom:.5rem">
An OCN requires a sponsoring CLEC agreement for numbering authority and interconnection. If you do not have an existing tandem/interconnection agreement, we will arrange one as part of this service.
</p>
<label style="font-size:.82rem;cursor:pointer">
<input type="checkbox" id="has-tandem"> I already have an existing tandem/interconnection agreement
</label>
</div>
<div style="margin-top:1rem;padding:.6rem;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0">
<div style="display:flex;justify-content:space-between;font-size:.88rem;padding:.15rem 0">
<span>OCN Registration</span><span style="font-weight:600">$650</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:.88rem;padding:.15rem 0" id="tandem-line">
<span>Sponsoring CLEC Agreement</span><span style="font-weight:600">$2,000</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:1rem;font-weight:700;padding:.4rem 0;border-top:1px solid #e2e8f0;margin-top:.3rem">
<span>Total</span><span id="total-price">$2,650</span>
</div>
</div>
<div style="margin-top:1rem;text-align:right">
<button class="btn btn-primary" id="btn-pay">Continue to Payment</button>
</div>
<p class="err" id="form-error"></p>
</div>
</div>
<script>
(function() {
var STATES = [
['AL','Alabama'],['AK','Alaska'],['AZ','Arizona'],['AR','Arkansas'],['CA','California'],
['CO','Colorado'],['CT','Connecticut'],['DE','Delaware'],['DC','District of Columbia'],['FL','Florida'],
['GA','Georgia'],['HI','Hawaii'],['ID','Idaho'],['IL','Illinois'],['IN','Indiana'],
['IA','Iowa'],['KS','Kansas'],['KY','Kentucky'],['LA','Louisiana'],['ME','Maine'],
['MD','Maryland'],['MA','Massachusetts'],['MI','Michigan'],['MN','Minnesota'],['MS','Mississippi'],
['MO','Missouri'],['MT','Montana'],['NE','Nebraska'],['NV','Nevada'],['NH','New Hampshire'],
['NJ','New Jersey'],['NM','New Mexico'],['NY','New York'],['NC','North Carolina'],['ND','North Dakota'],
['OH','Ohio'],['OK','Oklahoma'],['OR','Oregon'],['PA','Pennsylvania'],['RI','Rhode Island'],
['SC','South Carolina'],['SD','South Dakota'],['TN','Tennessee'],['TX','Texas'],['UT','Utah'],
['VT','Vermont'],['VA','Virginia'],['WA','Washington'],['WV','West Virginia'],['WI','Wisconsin'],['WY','Wyoming']
];
var API = window.__PW_API;
var selectedStates = new Set();
var picker = document.getElementById('state-picker');
// Render state chips
STATES.forEach(function(s) {
var chip = document.createElement('label');
chip.className = 'state-chip';
chip.innerHTML = '<input type="checkbox" value="'+s[0]+'"> '+s[0]+' '+s[1];
chip.querySelector('input').addEventListener('change', function(e) {
if (e.target.checked) { selectedStates.add(s[0]); chip.classList.add('selected'); }
else { selectedStates.delete(s[0]); chip.classList.remove('selected'); }
});
picker.appendChild(chip);
});
// Select all
document.getElementById('select-all-states').addEventListener('change', function() {
var checked = this.checked;
picker.querySelectorAll('input').forEach(function(cb) {
cb.checked = checked;
var code = cb.value;
if (checked) { selectedStates.add(code); cb.closest('.state-chip').classList.add('selected'); }
else { selectedStates.delete(code); cb.closest('.state-chip').classList.remove('selected'); }
});
});
// Tandem toggle — adjust price
document.getElementById('has-tandem').addEventListener('change', function() {
var line = document.getElementById('tandem-line');
var total = document.getElementById('total-price');
if (this.checked) {
line.style.textDecoration = 'line-through';
line.style.opacity = '0.5';
total.textContent = '$650';
} else {
line.style.textDecoration = '';
line.style.opacity = '';
total.textContent = '$2,650';
}
});
// Checkout
document.getElementById('btn-pay').addEventListener('click', async function() {
var btn = this;
var errEl = document.getElementById('form-error');
errEl.style.display = 'none';
var entityName = document.getElementById('entity-name').value.trim();
var contactName = document.getElementById('contact-name').value.trim();
var contactEmail = document.getElementById('contact-email').value.trim();
var contactPhone = document.getElementById('contact-phone').value.trim();
if (!entityName || !contactName || !contactEmail || !contactPhone) {
errEl.textContent = 'Please fill in all required fields (entity name, contact name, email, phone).';
errEl.style.display = 'block';
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contactEmail)) {
errEl.textContent = 'Please enter a valid email address.';
errEl.style.display = 'block';
return;
}
if (selectedStates.size === 0) {
errEl.textContent = 'Please select at least one operating state.';
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = 'Creating order...';
var hasTandem = document.getElementById('has-tandem').checked;
var orderBody = {
service_slug: 'ocn-registration',
customer_name: contactName,
customer_email: contactEmail,
customer_phone: contactPhone,
intake_data: {
entity_legal_name: entityName,
contact_name: contactName,
contact_email: contactEmail,
contact_phone: contactPhone,
frn: document.getElementById('frn').value.trim() || undefined,
service_category: document.getElementById('service-category').value,
operating_states: Array.from(selectedStates),
has_existing_tandem: hasTandem,
},
};
try {
// Step 1: Create compliance order
var orderResp = await fetch(API + '/api/v1/compliance-orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderBody),
});
var orderData = await orderResp.json();
if (!orderResp.ok) throw new Error(orderData.error || 'Order creation failed');
btn.textContent = 'Redirecting to payment...';
// Step 2: Create Stripe session
var sessionResp = await fetch(API + '/api/v1/checkout/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: orderData.id,
order_type: 'compliance',
payment_method: 'card',
}),
});
var sessionData = await sessionResp.json();
if (!sessionResp.ok) throw new Error(sessionData.error || 'Checkout failed');
if (sessionData.checkout_url) {
window.location.href = sessionData.checkout_url;
} else {
throw new Error('No checkout URL returned');
}
} catch (err) {
errEl.textContent = err.message || 'Something went wrong. Please try again.';
errEl.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Continue to Payment';
}
});
})();
</script>
</body>
</html>

View file

@ -0,0 +1,489 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="State PUC/PSC registration for VoIP, broadband, and CLEC providers. $399/state plus state filing fees. Bond procurement coordinated.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>State PUC/PSC Registration — Performance West Inc.</title>
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f9fafb;line-height:1.6}
a{color:#1e3a5f;text-decoration:none}
.wrap{max-width:780px;margin:0 auto;padding:2rem 1.25rem 4rem}
h1{font-size:1.65rem;font-weight:700;color:#111827;margin-bottom:.25rem}
.subtitle{font-size:.9rem;color:#6b7280;margin-bottom:1.5rem}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1.25rem;margin-bottom:1rem}
.card h2{font-size:1.1rem;font-weight:700;color:#1e3a5f;margin-bottom:.75rem}
.label{display:block;font-size:.82rem;font-weight:600;color:#374151;margin-bottom:.3rem}
select,input[type=text]{width:100%;padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:8px;font-size:.88rem;background:#fff}
select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px rgba(30,58,95,.15)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;font-size:.92rem;cursor:pointer;border:none;transition:all .15s}
.btn-primary{background:#1e3a5f;color:#fff}.btn-primary:hover{background:#162e4d}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.state-grid{display:grid;grid-template-columns:1fr;gap:.5rem;max-height:500px;overflow-y:auto;padding:.5rem 0}
@media(min-width:640px){.state-grid{grid-template-columns:1fr 1fr}}
.state-row{display:flex;align-items:center;gap:.6rem;padding:.55rem .7rem;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;transition:all .15s}
.state-row:hover{border-color:#1e3a5f;background:#f0f4f8}
.state-row.selected{border-color:#1e3a5f;background:#eff6ff}
.state-row.exempt{opacity:.55;cursor:default;background:#f9fafb}
.state-row input[type=checkbox]{accent-color:#1e3a5f;width:16px;height:16px;cursor:pointer}
.state-name{font-weight:600;font-size:.88rem;flex:1}
.state-meta{font-size:.75rem;color:#6b7280;text-align:right}
.state-meta .fee{color:#059669;font-weight:600}
.state-meta .bond{color:#d97706;font-size:.7rem}
.state-meta .exempt-tag{color:#9ca3af;font-style:italic}
.summary-box{position:sticky;bottom:0;background:#fff;border-top:2px solid #1e3a5f;padding:1rem 1.25rem;margin:-1.25rem;margin-top:1rem;border-radius:0 0 12px 12px}
.summary-row{display:flex;justify-content:space-between;font-size:.88rem;padding:.15rem 0}
.summary-row.total{font-weight:700;font-size:1.05rem;border-top:1px solid #e5e7eb;padding-top:.5rem;margin-top:.35rem}
.filter-bar{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.75rem}
.filter-btn{padding:.3rem .7rem;border:1px solid #d1d5db;border-radius:6px;font-size:.78rem;background:#fff;cursor:pointer;font-weight:500}
.filter-btn.active{background:#1e3a5f;color:#fff;border-color:#1e3a5f}
.hidden{display:none}
.info-banner{padding:.6rem .9rem;background:#ecfdf5;border-left:3px solid #059669;border-radius:0 6px 6px 0;font-size:.85rem;color:#065f46;margin-bottom:1.25rem}
.bond-banner{padding:.5rem .8rem;background:#fffbeb;border-left:3px solid #d97706;border-radius:0 6px 6px 0;font-size:.82rem;color:#92400e;margin-top:.75rem}
.step-dots{display:flex;gap:.75rem;margin-bottom:1.5rem}
.step-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.72rem;font-weight:700;background:#e5e7eb;color:#6b7280}
.step-dot.active{background:#1e3a5f;color:#fff}
.step-dot.done{background:#22c55e;color:#fff}
.entity-form input{margin-bottom:.75rem}
</style>
</head>
<body>
<div class="wrap">
<h1>State PUC/PSC Registration</h1>
<p class="subtitle">Register your VoIP, broadband, or CLEC service with state Public Utility Commissions. $399/state plus state filing fees.</p>
<div class="info-banner">
<strong>Tax deductible</strong> &mdash; State PUC registration fees are deductible as ordinary business expenses under IRC &sect; 162.
</div>
<div class="step-dots">
<div class="step-dot active" id="dot-1">1</div>
<div class="step-dot" id="dot-2">2</div>
<div class="step-dot" id="dot-3">3</div>
</div>
<!-- Step 1: Provider Info -->
<div id="step-1" class="card">
<h2>Provider Information</h2>
<div class="entity-form">
<label class="label">Legal Entity Name</label>
<input type="text" id="entity-name" placeholder="Acme Telecom LLC">
<label class="label">FRN (FCC Registration Number)</label>
<input type="text" id="frn" placeholder="0012345678">
<label class="label">Registration Type</label>
<select id="reg-type">
<option value="voip">VoIP / IPES Provider</option>
<option value="broadband">Broadband / ISP Provider</option>
<option value="clec">CLEC (Competitive Local Exchange Carrier)</option>
<option value="bundle">Bundle (VoIP + CLEC)</option>
</select>
<label class="label" style="margin-top:.5rem">Provider Classification</label>
<select id="provider-type">
<option value="">-- Select --</option>
<option value="facilities_based">Facilities-Based (own network infrastructure)</option>
<option value="reseller">Reseller (resell another carrier's service)</option>
<option value="over_the_top">Over-the-Top VoIP (internet-based, no PSTN interconnection)</option>
<option value="hybrid">Hybrid (own some infrastructure, resell some)</option>
</select>
<p style="font-size:.75rem;color:#6b7280;margin-top:.25rem">
Provider type affects bond requirements and registration category in many states. Resellers and OTT VoIP providers often have lower or no bond requirements.
</p>
</div>
<div style="margin-top:1rem;text-align:right">
<button class="btn btn-primary" id="btn-next-1">Select States &rarr;</button>
</div>
</div>
<!-- Step 2: State Selection -->
<div id="step-2" class="card hidden">
<h2>Select States</h2>
<p style="font-size:.85rem;color:#6b7280;margin-bottom:.75rem">
Check the states where you need PUC registration. Exempt states (no registration required) are grayed out.
</p>
<div class="filter-bar">
<button class="filter-btn active" data-filter="all">All States</button>
<button class="filter-btn" data-filter="required">Registration Required</button>
<button class="filter-btn" data-filter="bond">Bond Required</button>
<button class="filter-btn" data-filter="exempt">Exempt / No Reg</button>
</div>
<div style="margin-bottom:.5rem">
<label style="font-size:.82rem;cursor:pointer">
<input type="checkbox" id="select-all-required" style="accent-color:#1e3a5f"> Select all states requiring registration
</label>
</div>
<div class="state-grid" id="state-grid">
<div style="text-align:center;padding:2rem;color:#9ca3af;grid-column:1/-1">Loading state data...</div>
</div>
<div id="bond-banner" class="bond-banner hidden">
<strong>Bond required</strong> in <span id="bond-count">0</span> selected state(s). We coordinate surety bond procurement with our bonding partners. Bond amounts shown are typical ranges &mdash; exact amounts depend on your provider size and type.
</div>
<div class="summary-box" id="summary-box" style="display:none">
<div class="summary-row"><span>Selected states</span><span id="sum-states">0</span></div>
<div class="summary-row"><span>Service fees ($399/state)</span><span id="sum-service">$0</span></div>
<div class="summary-row"><span>State filing fees</span><span id="sum-state-fees">$0</span></div>
<div class="summary-row total"><span>Total</span><span id="sum-total">$0</span></div>
<div style="font-size:.72rem;color:#9ca3af;margin-top:.35rem">
Bond amounts not included in total &mdash; coordinated separately.
</div>
<div style="margin-top:.75rem;display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn" style="background:#e5e7eb;color:#374151" id="btn-back-2">&larr; Back</button>
<button class="btn btn-primary" id="btn-next-2" disabled>Review &amp; Pay &rarr;</button>
</div>
</div>
</div>
<!-- Step 3: Review & Pay -->
<div id="step-3" class="card hidden">
<h2>Review &amp; Checkout</h2>
<div id="review-content"></div>
<div style="margin-top:1.25rem;border-top:1px solid #e5e7eb;padding-top:1rem">
<h3 style="font-size:.95rem;font-weight:600;margin-bottom:.75rem">Contact Information</h3>
<label class="label">Full Name</label>
<input type="text" id="customer-name" placeholder="John Smith" style="margin-bottom:.5rem">
<label class="label">Email Address</label>
<input type="text" id="customer-email" placeholder="john@example.com" style="margin-bottom:.5rem">
<label class="label">Phone (optional)</label>
<input type="text" id="customer-phone" placeholder="(555) 123-4567" style="margin-bottom:.5rem">
<label class="label">Discount Code (optional)</label>
<input type="text" id="promo-code" placeholder="SAVE20" style="margin-bottom:.75rem">
</div>
<div style="margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn" style="background:#e5e7eb;color:#374151" id="btn-back-3">&larr; Back</button>
<button class="btn btn-primary" id="btn-pay" disabled>Continue to Payment</button>
</div>
<p id="checkout-error" style="color:#dc2626;font-size:.82rem;margin-top:.5rem;display:none"></p>
</div>
</div>
<script>
(function() {
const API = window.__PW_API;
let allStates = [];
let selected = new Set();
// Load state data
fetch(API + '/api/v1/puc/requirements/all')
.then(r => r.json())
.then(data => {
allStates = data.states || [];
renderGrid(allStates);
})
.catch(err => {
document.getElementById('state-grid').innerHTML =
'<div style="color:#dc2626;padding:1rem;grid-column:1/-1">Failed to load state data. Please refresh.</div>';
});
// Get the relevant fee for a state based on registration type
function getStateFee(s, regType) {
if (regType === 'broadband') return s.broadband_fee_cents || 0;
if (regType === 'clec') return s.clec_fee_cents || 0;
if (regType === 'bundle') return (s.voip_fee_cents || 0) + (s.clec_fee_cents || 0);
return s.voip_fee_cents || 0;
}
function getStateBond(s, regType) {
if (regType === 'clec' || regType === 'bundle') {
return { required: s.clec_bond_required || s.voip_bond_required, cents: Math.max(s.clec_bond_cents || 0, s.voip_bond_cents || 0) };
}
if (regType === 'broadband') return { required: false, cents: 0 };
return { required: s.voip_bond_required, cents: s.voip_bond_cents || 0 };
}
function renderGrid(states, filter) {
const grid = document.getElementById('state-grid');
grid.innerHTML = '';
const regType = document.getElementById('reg-type').value;
let filtered = states;
if (filter === 'required') filtered = states.filter(s => isRequired(s, regType));
else if (filter === 'bond') filtered = states.filter(s => getStateBond(s, regType).required && isRequired(s, regType));
else if (filter === 'exempt') filtered = states.filter(s => !isRequired(s, regType));
filtered.forEach(s => {
const req = isRequired(s, regType);
const fee = getStateFee(s, regType);
const bond = getStateBond(s, regType);
const row = document.createElement('label');
row.className = 'state-row' + (req ? '' : ' exempt') + (selected.has(s.state_code) ? ' selected' : '');
row.innerHTML = `
<input type="checkbox" value="${s.state_code}" ${req ? '' : 'disabled'} ${selected.has(s.state_code) ? 'checked' : ''}>
<span class="state-name">${s.state_name} (${s.state_code})</span>
<span class="state-meta">
${req ? `<span class="fee">$${(fee / 100).toLocaleString()}</span> fee` : '<span class="exempt-tag">No registration required</span>'}
${req && bond.required ? '<br><span class="bond">Bond: $' + (bond.cents / 100).toLocaleString() + '</span>' : ''}
</span>
`;
const cb = row.querySelector('input');
cb.addEventListener('change', () => {
if (cb.checked) { selected.add(s.state_code); row.classList.add('selected'); }
else { selected.delete(s.state_code); row.classList.remove('selected'); }
updateSummary();
});
grid.appendChild(row);
});
}
function isRequired(s, regType) {
if (regType === 'voip' || regType === 'bundle') return s.voip_required;
if (regType === 'broadband') return s.broadband_required;
if (regType === 'clec') return s.clec_required;
return s.voip_required;
}
function updateSummary() {
const box = document.getElementById('summary-box');
const regType = document.getElementById('reg-type').value;
if (selected.size === 0) {
box.style.display = 'none';
document.getElementById('btn-next-2').disabled = true;
document.getElementById('bond-banner').classList.add('hidden');
return;
}
box.style.display = 'block';
document.getElementById('btn-next-2').disabled = false;
let serviceFees = 0, stateFees = 0, bondTotal = 0, bondCount = 0;
selected.forEach(code => {
const s = allStates.find(st => st.state_code === code);
if (!s) return;
serviceFees += 39900;
stateFees += getStateFee(s, regType);
const bond = getStateBond(s, regType);
if (bond.required) { bondTotal += bond.cents; bondCount++; }
});
document.getElementById('sum-states').textContent = selected.size;
document.getElementById('sum-service').textContent = '$' + (serviceFees / 100).toLocaleString();
document.getElementById('sum-state-fees').textContent = '$' + (stateFees / 100).toLocaleString();
document.getElementById('sum-total').textContent = '$' + ((serviceFees + stateFees) / 100).toLocaleString();
if (bondCount > 0) {
document.getElementById('bond-banner').classList.remove('hidden');
document.getElementById('bond-count').textContent = bondCount;
} else {
document.getElementById('bond-banner').classList.add('hidden');
}
}
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderGrid(allStates, btn.dataset.filter);
});
});
// Select all required
document.getElementById('select-all-required').addEventListener('change', function() {
const regType = document.getElementById('reg-type').value;
if (this.checked) {
allStates.filter(s => isRequired(s, regType)).forEach(s => selected.add(s.state_code));
} else {
selected.clear();
}
renderGrid(allStates, document.querySelector('.filter-btn.active')?.dataset?.filter);
updateSummary();
});
// Re-render when reg type changes
document.getElementById('reg-type').addEventListener('change', () => {
selected.clear();
document.getElementById('select-all-required').checked = false;
renderGrid(allStates, document.querySelector('.filter-btn.active')?.dataset?.filter);
updateSummary();
});
// Step navigation
function showStep(n) {
[1,2,3].forEach(i => {
document.getElementById('step-' + i).classList.toggle('hidden', i !== n);
const dot = document.getElementById('dot-' + i);
dot.classList.remove('active','done');
if (i < n) dot.classList.add('done');
if (i === n) dot.classList.add('active');
});
}
document.getElementById('btn-next-1').addEventListener('click', () => {
const name = document.getElementById('entity-name').value.trim();
if (!name) { alert('Please enter your legal entity name.'); return; }
showStep(2);
// Re-render grid in case reg type changed
renderGrid(allStates);
updateSummary();
});
document.getElementById('btn-back-2').addEventListener('click', () => showStep(1));
document.getElementById('btn-next-2').addEventListener('click', () => {
showStep(3);
buildReview();
});
document.getElementById('btn-back-3').addEventListener('click', () => showStep(2));
function buildReview() {
const name = document.getElementById('entity-name').value.trim();
const frn = document.getElementById('frn').value.trim();
const regType = document.getElementById('reg-type').value;
const provType = document.getElementById('provider-type').value;
let html = `
<div style="margin-bottom:1rem">
<div style="font-size:.85rem;color:#6b7280">Entity</div>
<div style="font-weight:600">${esc(name)}</div>
${frn ? '<div style="font-size:.85rem;color:#6b7280">FRN: ' + esc(frn) + '</div>' : ''}
<div style="font-size:.85rem;color:#6b7280">Type: ${regType.toUpperCase()}${provType ? ' (' + provType.replace(/_/g,' ') + ')' : ''}</div>
</div>
<table style="width:100%;border-collapse:collapse;font-size:.85rem">
<thead><tr style="border-bottom:2px solid #e5e7eb">
<th style="text-align:left;padding:.4rem .5rem;font-weight:600">State</th>
<th style="text-align:right;padding:.4rem .5rem;font-weight:600">Filing Fee</th>
<th style="text-align:right;padding:.4rem .5rem;font-weight:600">Service Fee</th>
<th style="text-align:right;padding:.4rem .5rem;font-weight:600">Bond</th>
</tr></thead><tbody>
`;
let serviceTot = 0, stateTot = 0, bondTot = 0, bondStates = 0;
Array.from(selected).sort().forEach(code => {
const s = allStates.find(st => st.state_code === code);
if (!s) return;
const sf = getStateFee(s, regType);
const bond = getStateBond(s, regType);
const bondAmt = bond.required ? bond.cents : 0;
serviceTot += 39900;
stateTot += sf;
bondTot += bondAmt;
if (bond.required) bondStates++;
html += `<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:.35rem .5rem">${s.state_name} (${code})</td>
<td style="text-align:right;padding:.35rem .5rem">$${(sf/100).toLocaleString()}</td>
<td style="text-align:right;padding:.35rem .5rem">$399</td>
<td style="text-align:right;padding:.35rem .5rem;color:${bondAmt?'#d97706':'#9ca3af'}">${bondAmt ? '$'+(bondAmt/100).toLocaleString() : '---'}</td>
</tr>`;
});
html += `</tbody><tfoot>
<tr style="border-top:2px solid #e5e7eb;font-weight:700">
<td style="padding:.5rem">Total (${selected.size} states)</td>
<td style="text-align:right;padding:.5rem">$${(stateTot/100).toLocaleString()}</td>
<td style="text-align:right;padding:.5rem">$${(serviceTot/100).toLocaleString()}</td>
<td style="text-align:right;padding:.5rem;color:#d97706">${bondTot ? '$'+(bondTot/100).toLocaleString() : '---'}</td>
</tr>
</tfoot></table>
<div style="margin-top:.75rem;font-size:.92rem;font-weight:700;text-align:right">
Grand total: $${((serviceTot+stateTot)/100).toLocaleString()}
</div>
${bondTot > 0 ? '<div class="bond-banner" style="margin-top:.75rem"><strong>Bond total: $'+(bondTot/100).toLocaleString()+'</strong> across '+bondStates+' state(s). Bond procurement is coordinated separately after checkout.</div>' : ''}
`;
document.getElementById('review-content').innerHTML = html;
document.getElementById('btn-pay').disabled = false;
}
// Checkout — 2-step: create compliance order, then create Stripe session
document.getElementById('btn-pay').addEventListener('click', async () => {
const btn = document.getElementById('btn-pay');
const errEl = document.getElementById('checkout-error');
errEl.style.display = 'none';
const customerName = document.getElementById('customer-name').value.trim();
const customerEmail = document.getElementById('customer-email').value.trim();
if (!customerName || !customerEmail) {
errEl.textContent = 'Please enter your name and email address.';
errEl.style.display = 'block';
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerEmail)) {
errEl.textContent = 'Please enter a valid email address.';
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = 'Creating order...';
// Normalize provider_type: empty string -> null (DB CHECK constraint rejects "")
const provType = document.getElementById('provider-type').value || null;
const orderBody = {
service_slug: 'state-puc',
customer_name: customerName,
customer_email: customerEmail,
customer_phone: document.getElementById('customer-phone').value.trim() || undefined,
discount_code: document.getElementById('promo-code').value.trim() || undefined,
intake_data: {
entity_legal_name: document.getElementById('entity-name').value.trim(),
frn: document.getElementById('frn').value.trim(),
registration_type: document.getElementById('reg-type').value,
provider_type: provType,
target_states: Array.from(selected),
},
};
try {
// Step 1: Create compliance order
const orderResp = await fetch(API + '/api/v1/compliance-orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderBody),
});
const orderData = await orderResp.json();
if (!orderResp.ok) throw new Error(orderData.error || 'Order creation failed');
btn.textContent = 'Redirecting to payment...';
// Step 2: Create Stripe checkout session
const sessionResp = await fetch(API + '/api/v1/checkout/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: orderData.id,
order_type: 'compliance',
payment_method: 'card',
}),
});
const sessionData = await sessionResp.json();
if (!sessionResp.ok) throw new Error(sessionData.error || 'Checkout failed');
if (sessionData.checkout_url) {
window.location.href = sessionData.checkout_url;
} else {
throw new Error('No checkout URL returned');
}
} catch (err) {
errEl.textContent = err.message || 'Something went wrong. Please try again.';
errEl.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Continue to Payment';
}
});
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
})();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,351 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Review and approve your RMD certification before FCC submission.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Review RMD Filing — Performance West</title>
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/_astro/about.DhmoKVOS.css">
<style>
.rw{max-width:680px;margin:0 auto;padding:2rem 1rem 4rem}
.rh{background:#1e3a5f;color:#fff;border-radius:12px;padding:1.75rem 1.5rem;margin-bottom:1.5rem}
.rh h1{margin:0 0 .3rem;font-size:1.4rem;font-weight:700}
.rh p{margin:0;opacity:.8;font-size:.875rem}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:1.25rem 1.5rem;margin-bottom:1rem}
.card h2{font-size:.95rem;font-weight:700;color:#1e3a5f;margin:0 0 .75rem;display:flex;align-items:center;gap:.5rem}
.card h2 svg{width:18px;height:18px;color:#059669}
.fg{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.fg.single{grid-template-columns:1fr}
.fl{display:block;font-size:.7rem;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.fi{width:100%;border:1px solid #d1d5db;border-radius:6px;padding:8px 10px;font-size:13px;color:#111;outline:none;transition:border .15s}
.fi:focus{border-color:#1e3a5f;box-shadow:0 0 0 2px rgba(30,58,95,.15)}
select.fi{background:#fff}
.lb{background:#fefce8;border:1px solid #fde68a;border-radius:8px;padding:1rem;font-size:.8rem;color:#92400e;line-height:1.6}
.ab{width:100%;background:#059669;color:#fff;border:none;border-radius:10px;padding:.9rem;font-size:1rem;font-weight:700;cursor:pointer;transition:background .15s}
.ab:hover:not(:disabled){background:#047857}
.ab:disabled{opacity:.5;cursor:not-allowed}
.note{font-size:.7rem;color:#94a3b8;margin-top:2px}
</style>
</head>
<body class="min-h-screen flex flex-col bg-gray-50">
<nav class="border-b border-gray-200 bg-white sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4"><div class="flex justify-between h-16 items-center">
<a href="/" class="flex items-center"><img src="/images/logo.png" alt="Performance West" class="h-12 w-auto"></a>
</div></div>
</nav>
<main class="flex-1"><div class="rw">
<div id="loading" class="text-center py-16 text-gray-500">
<svg class="animate-spin h-6 w-6 mx-auto mb-3" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
Loading your filing details...
</div>
<div id="error" class="hidden text-center py-16">
<p class="text-red-700 font-semibold" id="error-msg"></p>
</div>
<div id="main-ui" class="hidden">
<div class="rh">
<h1>Review Your RMD Certification</h1>
<p>Please verify and correct all details below. We will use this information to prepare your FCC filing.</p>
</div>
<!-- Entity -->
<div class="card">
<h2><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/></svg>Entity Information</h2>
<div class="fg">
<div><label class="fl">Legal Name *</label><input type="text" class="fi" id="f-name" placeholder="Acme Telecom LLC"></div>
<div><label class="fl">DBA / Trade Name</label><input type="text" class="fi" id="f-dba" placeholder="Optional"></div>
<div><label class="fl">FRN *</label><input type="text" class="fi" id="f-frn" placeholder="0012345678" maxlength="10"></div>
<div><label class="fl">RMD Number</label><input type="text" class="fi" id="f-rmd-num" placeholder="RMD0001234 (if existing)"></div>
</div>
<div class="fg single" style="margin-top:.75rem">
<div><label class="fl">Principal Address *</label><input type="text" class="fi" id="f-address" placeholder="123 Main St, City, ST 12345"></div>
</div>
</div>
<!-- Classification -->
<div class="card">
<h2><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"/></svg>Provider Classification</h2>
<div class="fg">
<div>
<label class="fl">Carrier Category *</label>
<select class="fi" id="f-carrier-cat">
<option value="">Select...</option>
<option value="interconnected_voip">Interconnected VoIP</option>
<option value="non_interconnected_voip">Non-Interconnected VoIP</option>
<option value="clec">CLEC</option>
<option value="ixc">IXC</option>
<option value="cmrs">CMRS (Wireless)</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label class="fl">Infrastructure</label>
<select class="fi" id="f-infra">
<option value="facilities">Facilities-based</option>
<option value="reseller">Reseller</option>
<option value="hybrid">Hybrid</option>
</select>
</div>
</div>
<div class="fg" style="margin-top:.75rem">
<div><label class="fl">Wholesale provider?</label><select class="fi" id="f-wholesale"><option value="no">No</option><option value="yes">Yes</option></select></div>
<div><label class="fl">Gateway provider?</label><select class="fi" id="f-gateway"><option value="no">No</option><option value="yes">Yes</option></select></div>
</div>
</div>
<!-- STIR/SHAKEN -->
<div class="card">
<h2><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>STIR/SHAKEN Implementation</h2>
<div class="fg">
<div>
<label class="fl">Status *</label>
<select class="fi" id="f-ss-status">
<option value="complete_implementation">Complete Implementation</option>
<option value="partial_implementation">Partial Implementation</option>
<option value="robocall_mitigation_only">Robocall Mitigation Only</option>
<option value="exempt_small_carrier">Exempt (Small Carrier)</option>
<option value="not_applicable">Not Applicable</option>
</select>
</div>
<div><label class="fl">Certificate Authority</label><input type="text" class="fi" id="f-ss-ca" placeholder="e.g. TransNexus, Neustar"></div>
</div>
<div class="fg single" style="margin-top:.75rem">
<div><label class="fl">Upstream Provider (if partial/mitigation only)</label><input type="text" class="fi" id="f-ss-upstream" placeholder="Provider name"></div>
</div>
</div>
<!-- Contact -->
<div class="card">
<h2><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"/></svg>Contact &amp; Certifying Officer</h2>
<div class="fg">
<div><label class="fl">Contact Name *</label><input type="text" class="fi" id="f-contact-name" placeholder="Jane Doe"></div>
<div><label class="fl">Contact Email *</label><input type="email" class="fi" id="f-contact-email" placeholder="jane@company.com"></div>
<div><label class="fl">Contact Phone</label><input type="tel" class="fi" id="f-contact-phone" placeholder="(555) 123-4567"></div>
<div><label class="fl">Contact Title</label><input type="text" class="fi" id="f-contact-title" placeholder="VP Regulatory"></div>
</div>
<div class="fg" style="margin-top:.75rem">
<div><label class="fl">Certifying Officer Name *</label><input type="text" class="fi" id="f-officer-name" placeholder="CEO / President name"></div>
<div><label class="fl">Certifying Officer Title *</label><input type="text" class="fi" id="f-officer-title" placeholder="Chief Executive Officer"></div>
</div>
<p class="note">The certifying officer signs the RMD filing under penalty of perjury (47 CFR &sect; 1.16).</p>
</div>
<!-- Step 1: Preview -->
<div class="card" id="preview-card">
<h2><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>Preview Your Filing</h2>
<p style="font-size:.85rem;color:#64748b;margin-bottom:1rem">Click below to generate a preview of your RMD certification letter based on the information you provided. Review it carefully before approving.</p>
<button type="button" class="ab" id="preview-btn" style="background:#1e3a5f">Generate Preview</button>
<p id="preview-status" style="font-size:.85rem;margin-top:.5rem;text-align:center;color:#64748b"></p>
<div id="pdf-viewer" style="display:none;margin-top:1rem">
<iframe id="pdf-frame" style="width:100%;height:600px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc"></iframe>
</div>
</div>
<!-- Step 2: Approve (shown after preview) -->
<div class="card" id="approve-card" style="display:none">
<h2>Authorization</h2>
<div class="lb" id="legal-text"></div>
<div style="margin-top:1rem;display:flex;gap:.75rem;align-items:flex-start;padding:1rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;font-size:.875rem">
<input type="checkbox" id="agree-chk" style="margin-top:2px;width:18px;height:18px;flex-shrink:0">
<label for="agree-chk" style="color:#374151;line-height:1.5">
I have reviewed the preview above and confirm that the information is true, complete, and accurate.
I authorize Performance West Inc. to submit this RMD certification to the FCC on my behalf.
I understand that the 47 CFR &sect; 1.16 declaration under penalty of perjury will be made
through the FCC electronic filing portal at the time of submission.
</label>
</div>
<p id="status-msg" style="font-size:.875rem;margin-top:.75rem;min-height:1rem;text-align:center"></p>
<button class="ab" id="approve-btn" disabled>Approve &amp; Authorize Filing</button>
</div>
</div>
<div id="success" class="hidden text-center py-16">
<div style="width:56px;height:56px;background:#dcfce7;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1rem">
<svg width="28" height="28" fill="none" stroke="#16a34a" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path></svg>
</div>
<h2 class="text-xl font-bold text-gray-900 mb-2">Filing Approved</h2>
<p class="text-sm text-gray-600 max-w-md mx-auto">Your RMD certification has been approved and will be submitted to the FCC shortly. You will receive an email with the confirmation number once filed.</p>
</div>
</div></main>
<footer class="border-t border-gray-200 bg-white mt-auto py-6">
<div class="max-w-6xl mx-auto px-4 text-center">
<img src="/images/logo.png" alt="Performance West" class="h-8 w-auto mx-auto mb-2 opacity-50">
<p class="text-xs text-gray-500">&copy; 2026 Performance West Inc. &middot; 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001</p>
</div>
</footer>
<script>
(function() {
var API = window.__PW_API;
var token = new URLSearchParams(window.location.search).get("token");
if (!token) {
document.getElementById("loading").classList.add("hidden");
document.getElementById("error").classList.remove("hidden");
document.getElementById("error-msg").textContent = "No authentication token. Please use the link from your email.";
return;
}
document.cookie = "pw_portal_token=" + token + ";path=/;max-age=259200;SameSite=Lax";
var headers = {"Authorization": "Bearer " + token, "Content-Type": "application/json"};
fetch(API + "/api/v1/portal/rmd-review", {headers: headers})
.then(function(r) { return r.json().then(function(d) { if (!r.ok) throw new Error(d.error || "Failed"); return d; }); })
.then(function(data) {
document.getElementById("loading").classList.add("hidden");
if (data.review_status === "approved") {
document.getElementById("success").classList.remove("hidden");
return;
}
document.getElementById("main-ui").classList.remove("hidden");
var e = data.entity || {};
var cl = data.classification || {};
var ss = data.stir_shaken || {};
var ct = data.contact || {};
var co = data.certifying_officer || {};
// Prefill all fields
document.getElementById("f-name").value = e.legal_name || "";
document.getElementById("f-dba").value = e.dba_name || "";
document.getElementById("f-frn").value = e.frn || (data.intake_frn || "");
document.getElementById("f-rmd-num").value = e.rmd_number || "";
document.getElementById("f-address").value = e.address || "";
if (cl.carrier_category) document.getElementById("f-carrier-cat").value = cl.carrier_category;
if (cl.infra_type) document.getElementById("f-infra").value = cl.infra_type;
if (cl.is_wholesale) document.getElementById("f-wholesale").value = "yes";
if (cl.gateway_provider) document.getElementById("f-gateway").value = "yes";
if (ss.status) document.getElementById("f-ss-status").value = ss.status;
document.getElementById("f-ss-ca").value = ss.cert_authority || "";
document.getElementById("f-ss-upstream").value = ss.upstream_provider || "";
document.getElementById("f-contact-name").value = ct.name || "";
document.getElementById("f-contact-email").value = ct.email || "";
document.getElementById("f-contact-phone").value = ct.phone || "";
document.getElementById("f-contact-title").value = ct.title || "";
document.getElementById("f-officer-name").value = co.name || "";
document.getElementById("f-officer-title").value = co.title || "Chief Executive Officer";
document.getElementById("legal-text").textContent = data.legal_notice || "";
// Preview button handler
document.getElementById("preview-btn").addEventListener("click", function() {
var previewBtn = document.getElementById("preview-btn");
var previewStatus = document.getElementById("preview-status");
previewBtn.disabled = true;
previewBtn.textContent = "Generating preview...";
previewStatus.textContent = "This may take 15-30 seconds. Please wait.";
var corrections = getCorrections();
fetch(API + "/api/v1/portal/rmd-preview", {
method: "POST",
headers: headers,
body: JSON.stringify({corrections: corrections}),
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.pdf_url) {
document.getElementById("pdf-viewer").style.display = "block";
document.getElementById("pdf-frame").src = d.pdf_url;
previewStatus.textContent = "Preview generated. Review the document below, then approve.";
previewStatus.style.color = "#059669";
previewBtn.textContent = "Regenerate Preview";
previewBtn.disabled = false;
// Show the approve card
document.getElementById("approve-card").style.display = "block";
// Scroll to PDF
document.getElementById("pdf-viewer").scrollIntoView({behavior: "smooth"});
} else {
previewStatus.textContent = d.error || "Could not generate preview.";
previewStatus.style.color = "#dc2626";
previewBtn.textContent = "Try Again";
previewBtn.disabled = false;
}
})
.catch(function(err) {
previewStatus.textContent = err.message || "Preview failed.";
previewStatus.style.color = "#dc2626";
previewBtn.textContent = "Try Again";
previewBtn.disabled = false;
});
});
// Checkbox enables approve button
var chk = document.getElementById("agree-chk");
var btn = document.getElementById("approve-btn");
chk.addEventListener("change", function() { btn.disabled = !chk.checked; });
function getCorrections() {
return {
legal_name: document.getElementById("f-name").value.trim(),
dba_name: document.getElementById("f-dba").value.trim(),
frn: document.getElementById("f-frn").value.trim(),
rmd_number: document.getElementById("f-rmd-num").value.trim(),
address: document.getElementById("f-address").value.trim(),
carrier_category: document.getElementById("f-carrier-cat").value,
infra_type: document.getElementById("f-infra").value,
is_wholesale: document.getElementById("f-wholesale").value === "yes",
is_gateway_provider: document.getElementById("f-gateway").value === "yes",
stir_shaken_status: document.getElementById("f-ss-status").value,
stir_shaken_cert_authority: document.getElementById("f-ss-ca").value.trim(),
upstream_provider: document.getElementById("f-ss-upstream").value.trim(),
contact_name: document.getElementById("f-contact-name").value.trim(),
contact_email: document.getElementById("f-contact-email").value.trim(),
contact_phone: document.getElementById("f-contact-phone").value.trim(),
contact_title: document.getElementById("f-contact-title").value.trim(),
ceo_name: document.getElementById("f-officer-name").value.trim(),
ceo_title: document.getElementById("f-officer-title").value.trim(),
};
}
// Approve + submit corrections
btn.addEventListener("click", function() {
btn.disabled = true;
btn.textContent = "Submitting...";
document.getElementById("status-msg").textContent = "";
var corrections = getCorrections();
fetch(API + "/api/v1/portal/rmd-approve", {
method: "POST",
headers: headers,
body: JSON.stringify({corrections: corrections}),
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.status === "approved" || d.status === "already_approved") {
document.getElementById("main-ui").classList.add("hidden");
document.getElementById("success").classList.remove("hidden");
} else {
document.getElementById("status-msg").textContent = d.error || "Something went wrong.";
document.getElementById("status-msg").style.color = "#dc2626";
btn.disabled = false;
btn.textContent = "Approve & Authorize Filing";
}
})
.catch(function(err) {
document.getElementById("status-msg").textContent = err.message;
document.getElementById("status-msg").style.color = "#dc2626";
btn.disabled = false;
btn.textContent = "Approve & Authorize Filing";
});
});
})
.catch(function(err) {
document.getElementById("loading").classList.add("hidden");
document.getElementById("error").classList.remove("hidden");
document.getElementById("error-msg").textContent = err.message || "Could not load filing details.";
});
})();
</script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
site/public/robots.txt Normal file
View file

@ -0,0 +1,20 @@
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: CCBot
Allow: /
User-agent: *
Allow: /
Disallow: /admin
Sitemap: https://performancewest.net/sitemap-index.xml

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more