<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>P4 Survey Questionnaire</title>
<style>
:root {
color: #1f2933;
background: #e8f0f2;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
* { box-sizing: border-box; }
body { margin: 0; }
main {
width: min(100% - 2rem, 70rem);
margin: 2rem auto;
background: #fff;
border-top: 5px solid #0d6b85;
box-shadow: 0 0.75rem 2.5rem rgb(31 41 51 / 12%);
}
header {
padding: 2rem 2.25rem 1.5rem;
border-bottom: 1px solid #d6e1e6;
}
h1, h2 { color: #173f52; line-height: 1.2; }
h1 { margin: 0; font-size: 2rem; }
h2 { margin: 0 0 1rem; font-size: 1.25rem; }
p { margin: 0.75rem 0 0; }
.content {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(18rem, 0.85fr);
gap: 2rem;
padding: 2rem 2.25rem 2.25rem;
}
form { display: grid; gap: 1.5rem; }
fieldset { margin: 0; padding: 1.25rem; border: 1px solid #c6d7dc; }
legend { padding: 0 0.4rem; color: #173f52; font-weight: 700; }
.fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.full { grid-column: 1 / -1; }
label { display: grid; gap: 0.35rem; font-weight: 650; }
input, select, textarea, button { font: inherit; }
input, select, textarea {
width: 100%;
padding: 0.55rem 0.65rem;
border: 1px solid #9eb7c0;
border-radius: 3px;
color: #1f2933;
background: #fff;
}
input:focus, select:focus, textarea:focus { outline: 3px solid rgb(13 107 133 / 25%); border-color: #0d6b85; }
textarea { min-height: 7rem; resize: vertical; }
.options { display: grid; gap: 0.55rem; }
.option { display: flex; align-items: center; gap: 0.55rem; font-weight: 400; }
.option input { width: auto; margin: 0; }
.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
button { padding: 0.65rem 1rem; border: 1px solid transparent; border-radius: 3px; cursor: pointer; font-weight: 700; }
button[type="submit"] { color: #fff; background: #0d6b85; }
button[type="submit"]:hover { background: #07546b; }
button[type="reset"] { color: #173f52; background: #edf2f4; border-color: #9eb7c0; }
aside { min-width: 0; }
.preview {
min-height: 24rem;
margin: 0;
padding: 1rem;
overflow: auto;
white-space: pre-wrap;
border: 1px solid #c6d7dc;
background: #f6fafb;
color: #263a44;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.875rem;
line-height: 1.45;
}
.note { color: #4d6572; font-size: 0.9rem; }
@media (max-width: 760px) {
main { width: 100%; margin: 0; box-shadow: none; }
header, .content { padding: 1.5rem; }
.content { grid-template-columns: 1fr; }
.fields { grid-template-columns: 1fr; }
.full { grid-column: auto; }
}
.content { align-items: start; }
#questionnaire { gap: 1.25rem; }
fieldset { min-inline-size: 0; }
.question { display: grid; gap: 0.4rem; }
.question + .question { margin-top: 1rem; }
.question label { font-weight: 650; }
.prompt { margin: 0; color: #263a44; }
.helper { margin: 0; color: #4d6572; font-size: 0.9rem; }
.question textarea { min-height: 6.5rem; }
.question textarea.short { min-height: 4.25rem; }
aside { position: sticky; top: 1rem; }
.preview { min-height: 32rem; max-height: calc(100vh - 10rem); }
@media (max-width: 760px) {
aside { position: static; }
.preview { max-height: none; }
}
</style>
</head>
<body>
<main>
<header>
<h1>Pre-Consulting Services Site Survey</h1>
<p><strong>Download this questionnaire and open the downloaded file in a browser before completing it.</strong> It requires JavaScript and cannot be completed when displayed in Swarm/Code Review.</p>
<p>Capture the deployment context that accompanies a <code>p4survey</code> report. Answer what you can and return it promptly; responses stay in this browser and download as a Markdown file so that you can email it to us.</p>
<p class="note"><a href="../README.html#Download">Back to the p4survey README where you can download the tool.</a></p>
</header>
<div class="content">
<form id="questionnaire" novalidate>
<div id="questions"></div>
<div class="actions">
<button type="submit">Download Markdown</button>
<button type="reset">Clear form</button>
</div>
</form>
<aside aria-label="Markdown preview">
<h2>Markdown Preview</h2>
<pre class="preview" id="preview"></pre>
<p class="note">The preview updates as you type. Download uses your browser's local save location.</p>
</aside>
</div>
</main>
<script>
(() => {
const form = document.querySelector('#questionnaire');
const preview = document.querySelector('#preview');
const sections = [
{
title: 'Customer Details',
questions: [
{ label: 'Customer company', name: 'company', type: 'text' },
{ label: 'Primary contact', helper: 'Email address, name, and job title.', name: 'contact1', type: 'text' }
]
},
{
title: '1. Customer Perspective',
questions: [
{ label: 'What prompted this survey?', name: 'purpose', type: 'select', options: ['Routine health review', 'Performance investigation', 'Upgrade or migration planning', 'Support request', 'Other'] },
{ label: 'What should the review focus on?', name: 'focus', type: 'checkboxes', options: ['Performance and capacity', 'High availability and recovery', 'Security and access controls', 'Upgrade or migration readiness'] },
{ label: '1.1 Issues Reported', helper: 'What are the main issues Perforce users and administrators report? If possible include impact, frequency, and examples.', name: 'issues', type: 'textarea' },
{ label: '1.2 Goals Identified', helper: 'What goals support continued Perforce growth and usability?', name: 'goals', type: 'textarea' },
{ label: 'Current symptoms, risks, or questions', helper: 'Describe observed issues, timing, impact, or specific questions.', name: 'concerns', type: 'textarea' },
{ label: 'Planned changes or relevant dates', helper: 'Include planned upgrades, maintenance windows, growth expectations, or deadlines.', name: 'changes', type: 'textarea' }
]
},
{
title: '2. Perforce Architecture',
questions: [
{ label: '2.1 User Locations', helper: 'Where are users geo-located relative to the Perforce servers? Include approximate user totals by location.', name: 'userLocations', type: 'textarea', short: true }
]
},
{
title: '3. Perforce Server Overview',
questions: [
{ label: '3.1 Hardware or Operating System Issues', helper: 'Note particular or recent hardware and operating-system problems.', name: 'serverIssues', type: 'textarea', short: true },
{ label: '3.2 Server Monitoring', helper: 'What tool(s) are used for server monitoring? Include a weekly CPU screenshot if available and user perception of performance.', name: 'monitoring', type: 'textarea' },
{ label: '3.3 Server Access', helper: 'How do users and administrators access the server: LAN, VPN, or whitelisted IP addresses?', name: 'serverAccess', type: 'textarea', short: true },
{ label: '3.4 Recent Outages', helper: 'How many unplanned outages or significant service disruptions occurred in the last week and last month? Briefly describe the cause and impact.', name: 'recentOutages', type: 'textarea', short: true }
]
},
{
title: '4. Supporting Software and Internal Tools',
questions: [
{ label: '4.1 Supporting Software, Internal Tools, and Issues', helper: 'Summarise backup, defect tracking, build tools, anti-virus, in-house tools. Note any associated issues or Perforce interactions.', name: 'supportingTools', type: 'textarea' }
]
},
{
title: '5. Perforce Administration Practices',
questions: [
{ label: '5.1 Automated Backups', helper: 'How often are backups taken, and where are they stored? Are backup success and restore capability checked?', name: 'automatedBackups', type: 'textarea', short: true },
{ label: '5.2 Redundant Data Storage', helper: 'Describe off-site data storage and transfer frequency.', name: 'offsiteStorage', type: 'textarea', short: true },
{ label: '5.3 Failover', helper: 'Is failover available? Briefly describe the process and when it was last tested.', name: 'failover', type: 'textarea', short: true },
{ label: '5.4 Disaster Recovery', helper: 'Is there a tested disaster-recovery plan? Include the expected recovery time and acceptable data loss, if known.', name: 'disasterRecovery', type: 'textarea', short: true },
{ label: '5.5 Perforce Upgrades', helper: 'How often is Perforce upgraded, and how are upgrades tested before production?', name: 'upgrades', type: 'textarea', short: true }
]
},
{
title: '6. Miscellaneous',
questions: [
{ label: '6.1 Additional Items', helper: 'Add any useful details not addressed above.', name: 'additionalItems', type: 'textarea' }
]
}
];
const questions = document.querySelector('#questions');
const escapeHtml = text => text.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]);
const field = question => {
const helper = question.helper ? `<p class="helper">${escapeHtml(question.helper)}</p>` : '';
let control;
if (question.type === 'textarea') {
control = `<textarea id="${question.name}" name="${question.name}"${question.short ? ' class="short"' : ''}></textarea>`;
} else if (question.type === 'select') {
control = `<select id="${question.name}" name="${question.name}">${question.options.map(option => `<option>${escapeHtml(option)}</option>`).join('')}</select>`;
} else if (question.type === 'checkboxes') {
control = `<div class="options">${question.options.map(option => `<label class="option"><input name="${question.name}" type="checkbox" value="${escapeHtml(option)}">${escapeHtml(option)}</label>`).join('')}</div>`;
} else {
control = `<input id="${question.name}" name="${question.name}" type="text">`;
}
return `<div class="question"><label for="${question.name}">${escapeHtml(question.label)}</label>${helper}${control}</div>`;
};
questions.innerHTML = sections.map(section => `<fieldset><legend>${escapeHtml(section.title)}</legend>${section.questions.map(field).join('')}</fieldset>`).join('');
const value = name => form.elements[name].value.trim().replace(/\r\n/g, '\n');
const response = question => question.type === 'checkboxes'
? [...form.querySelectorAll(`input[name="${question.name}"]:checked`)].map(input => `- ${input.value}`).join('\n')
: value(question.name);
function output() {
const lines = [
'# Perforce Consulting Services Site Survey',
'',
`- Completed: ${new Date().toISOString().slice(0, 10)}`,
''
];
sections.forEach(section => {
lines.push(`## ${section.title}`, '');
section.questions.forEach(question => {
lines.push(`### ${question.label}`, '', response(question) || '[not provided]', '');
});
});
return lines.join('\n');
}
function updatePreview() {
preview.textContent = output();
}
form.addEventListener('input', updatePreview);
form.addEventListener('change', updatePreview);
form.addEventListener('reset', () => requestAnimationFrame(updatePreview));
form.addEventListener('submit', event => {
event.preventDefault();
const blob = new Blob([output()], { type: 'text/markdown;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
const filenamePart = (value('company') || 'perforce').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
link.download = `${filenamePart || 'perforce'}-site-survey.md`;
link.hidden = true;
document.body.append(link);
link.click();
setTimeout(() => {
URL.revokeObjectURL(link.href);
link.remove();
}, 0);
});
updatePreview();
})();
</script>
</body>
</html>