Compare commits

...

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

51 changed files with 479 additions and 6713 deletions

View file

@ -1,7 +0,0 @@
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-15T18:56:06.304Z","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","content":"hello"}
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-15T18:56:06.305Z","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399"}
{"parentUuid":null,"isSidechain":false,"promptId":"66ae766f-c1d3-410d-ae3e-b981c0724f21","type":"user","message":{"role":"user","content":"hello"},"uuid":"1e762f24-47aa-4036-9a94-8971b320b1c4","timestamp":"2026-04-15T18:56:06.454Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
{"parentUuid":"1e762f24-47aa-4036-9a94-8971b320b1c4","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"d095c33b-07ad-4e15-a52c-aa116a7cb2fa","timestamp":"2026-04-15T18:56:06.453Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
{"parentUuid":"d095c33b-07ad-4e15-a52c-aa116a7cb2fa","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"d2e44913-c8b0-4ab7-8fc9-8bb0ab8ba863","timestamp":"2026-04-15T18:56:06.453Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
{"parentUuid":"d2e44913-c8b0-4ab7-8fc9-8bb0ab8ba863","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01WwmhgEvkxkYCsd7hXfhbKG","type":"message","role":"assistant","content":[{"type":"text","text":"Hello! How can I help you today?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4742,"cache_read_input_tokens":11877,"output_tokens":12,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":4742},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":12,"cache_read_input_tokens":11877,"cache_creation_input_tokens":4742,"cache_creation":{"ephemeral_5m_input_tokens":4742,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca64pEERr5BfpcFu72ehk","type":"assistant","uuid":"8f1ae4f9-0b1d-4d74-bbe4-1b0112f3f259","timestamp":"2026-04-15T18:56:07.826Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
{"type":"last-prompt","lastPrompt":"hello","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399"}

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T05:31:24.106Z","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","content":"hello"}
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T05:31:24.107Z","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453"}
{"parentUuid":null,"isSidechain":false,"promptId":"733c4645-7420-4083-a8d9-f02e3a109bcb","type":"user","message":{"role":"user","content":"hello"},"uuid":"228cfd6f-7aca-446d-899f-ec53687a2036","timestamp":"2026-04-16T05:31:24.266Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"228cfd6f-7aca-446d-899f-ec53687a2036","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"506972de-2be9-4015-8118-71b1d369045f","timestamp":"2026-04-16T05:31:24.265Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"506972de-2be9-4015-8118-71b1d369045f","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"75dcf71c-c24a-42fa-a4fd-80f37473a551","timestamp":"2026-04-16T05:31:24.265Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"75dcf71c-c24a-42fa-a4fd-80f37473a551","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01VXSefWtX3DKqx9aHW2UdHi","type":"message","role":"assistant","content":[{"type":"text","text":"Ahoj! Jak ti můžu pomoct?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7263,"cache_read_input_tokens":11877,"output_tokens":16,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7263},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":16,"cache_read_input_tokens":11877,"cache_creation_input_tokens":7263,"cache_creation":{"ephemeral_5m_input_tokens":7263,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca6uG4DV96LqFZMvx9go7","type":"assistant","uuid":"9d7646ca-bff7-4cc1-8ec7-99c8e4fde647","timestamp":"2026-04-16T05:31:25.566Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
{"type":"last-prompt","lastPrompt":"hello","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453"}

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T05:31:09.921Z","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","content":"hello"}
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T05:31:09.922Z","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514"}
{"parentUuid":null,"isSidechain":false,"promptId":"1ac9be3b-4664-42a9-9890-085b4ce24690","type":"user","message":{"role":"user","content":"hello"},"uuid":"1d92e5f9-131a-4af0-bec5-54480d51e923","timestamp":"2026-04-16T05:31:10.163Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"1d92e5f9-131a-4af0-bec5-54480d51e923","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"9bd20a36-2eb7-42ff-8444-8f3f0e4f87fb","timestamp":"2026-04-16T05:31:10.162Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"9bd20a36-2eb7-42ff-8444-8f3f0e4f87fb","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"9f5dd7c2-375c-4d65-99fa-852b77d6f336","timestamp":"2026-04-16T05:31:10.162Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"9f5dd7c2-375c-4d65-99fa-852b77d6f336","isSidechain":false,"type":"assistant","uuid":"6792e80d-c530-441b-8d46-44182aff574f","timestamp":"2026-04-16T05:31:10.222Z","message":{"id":"167497be-9d34-45dd-b1be-faa189c74034","container":null,"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"Not logged in · Please run /login"}],"context_management":null},"error":"authentication_failed","isApiErrorMessage":true,"userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
{"type":"last-prompt","lastPrompt":"hello","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514"}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
{"type":"permission-mode","permissionMode":"bypassPermissions","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}
{"type":"file-history-snapshot","messageId":"c33f10bb-3085-442c-a697-657579e695df","snapshot":{"messageId":"c33f10bb-3085-442c-a697-657579e695df","trackedFileBackups":{},"timestamp":"2026-04-16T05:30:32.482Z"},"isSnapshotUpdate":false}
{"parentUuid":null,"isSidechain":false,"promptId":"9dbdde24-7495-438b-abbf-70c4418cbf22","type":"user","message":{"role":"user","content":"shrň aktuálnní stav a pojďme plánovat dnešek"},"uuid":"c33f10bb-3085-442c-a697-657579e695df","timestamp":"2026-04-16T05:30:32.482Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"c33f10bb-3085-442c-a697-657579e695df","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"5f7d7dc7-d196-48da-ab8e-04f31de266c3","timestamp":"2026-04-16T05:30:32.480Z","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"5f7d7dc7-d196-48da-ab8e-04f31de266c3","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"2125fb51-964a-4ffa-9f81-9f768e18ea06","timestamp":"2026-04-16T05:30:32.480Z","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"2125fb51-964a-4ffa-9f81-9f768e18ea06","isSidechain":false,"type":"assistant","uuid":"3c7ff654-8f1f-484b-acbd-f5fc7b7ae6ab","timestamp":"2026-04-16T05:30:32.555Z","message":{"id":"14b11517-d5d8-4616-8e78-f427f214c837","container":null,"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"Not logged in · Please run /login"}],"context_management":null},"error":"authentication_failed","isApiErrorMessage":true,"userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
{"type":"last-prompt","lastPrompt":"shrň aktuálnní stav a pojďme plánovat dnešek","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}
{"type":"permission-mode","permissionMode":"bypassPermissions","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}

View file

@ -1,7 +0,0 @@
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T06:09:34.704Z","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","content":"hello"}
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T06:09:34.704Z","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3"}
{"parentUuid":null,"isSidechain":false,"promptId":"0d818749-ad6c-4820-b5b2-2feaf7649ad8","type":"user","message":{"role":"user","content":"hello"},"uuid":"30d2e6d5-0cde-49a4-a649-54567315d0f7","timestamp":"2026-04-16T06:09:34.857Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"30d2e6d5-0cde-49a4-a649-54567315d0f7","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"24063f49-fa5f-4064-b778-ed196a8faf6d","timestamp":"2026-04-16T06:09:34.856Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"24063f49-fa5f-4064-b778-ed196a8faf6d","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"195c31bf-837a-4c76-8577-52e7013962d4","timestamp":"2026-04-16T06:09:34.856Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
{"parentUuid":"195c31bf-837a-4c76-8577-52e7013962d4","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01CbdnUgvNLh5zj337pQznQK","type":"message","role":"assistant","content":[{"type":"text","text":"Ahoj! Jak mohu pomoci?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7263,"cache_read_input_tokens":11877,"output_tokens":14,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7263},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":14,"cache_read_input_tokens":11877,"cache_creation_input_tokens":7263,"cache_creation":{"ephemeral_5m_input_tokens":7263,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca6xAuUbG4ihREHZN9SF1","type":"assistant","uuid":"4862843a-e4de-4928-a0d3-795303fac911","timestamp":"2026-04-16T06:09:36.076Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
{"type":"last-prompt","lastPrompt":"hello","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3"}

File diff suppressed because one or more lines are too long

View file

@ -1,81 +0,0 @@
name: release
on:
push:
branches: [main]
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'scripts/**'
- 'assets/**'
- 'embed.go'
- 'cmd/xetup/app.manifest'
- '.forgejo/workflows/release.yml'
jobs:
build-and-release:
# Runner label 'ubuntu-latest' maps to golang:1.24-alpine container (see runner config)
runs-on: ubuntu-latest
defaults:
run:
shell: sh
working-directory: /repo
steps:
- name: Setup
working-directory: /
run: |
apk add --no-cache git curl jq
git clone --depth=1 \
"http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \
/repo
cd /repo
git checkout "${{ github.sha }}"
- name: Generate rsrc.syso (manifest + UAC)
run: |
go install github.com/akavel/rsrc@latest
rsrc -manifest cmd/xetup/app.manifest -o cmd/xetup/rsrc.syso
echo "rsrc.syso: $(ls -lh cmd/xetup/rsrc.syso | awk '{print $5}')"
- name: Build xetup.exe
run: |
GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
- name: Publish latest release
env:
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
API: http://xetup-forgejo:3000/api/v1
REPO: ${{ github.repository }}
run: |
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
# Delete existing 'latest' release and tag to recreate cleanly
RID=$(curl -sf -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/tags/latest" | jq -r '.id // empty')
if [ -n "$RID" ]; then
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$RID" || true
fi
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/tags/latest" || true
# Create new 'latest' release
RID=$(curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "{\"tag_name\":\"latest\",\"name\":\"latest\",\"body\":\"Auto-built from ${SHORT}\",\"prerelease\":true}" \
| jq -r '.id')
# Upload xetup.exe
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
"$API/repos/$REPO/releases/$RID/assets?name=xetup.exe" \
--data-binary @xetup.exe
echo "Released xetup.exe (commit ${SHORT})"

4
.gitignore vendored
View file

@ -21,7 +21,3 @@ flash.zip
# Large reference files
W11.pdf
# Claude Code runtime artifacts
.claude/scheduled_tasks.lock
web/data/deploy.json

View file

@ -113,7 +113,6 @@ $stepsEnabled = @{
network = $true
pcIdentity = $true
activation = $true
dellUpdate = $true
}
if ($Config -and $Config.steps) {
foreach ($key in @($stepsEnabled.Keys)) {
@ -222,15 +221,6 @@ if ($stepsEnabled['network']) {
}
} else { Skip-Step "Step 9 - Network" }
# -----------------------------------------------------------------------
# Step 11 - Dell Command | Update (auto-skipped on non-Dell hardware)
# -----------------------------------------------------------------------
if ($stepsEnabled['dellUpdate']) {
Invoke-Step -Name "Step 11 - Dell Command | Update" -Action {
& "$ScriptRoot\scripts\11-dell-update.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 11 - Dell Command | Update" }
# -----------------------------------------------------------------------
# Step 10 - PC identity (rename + C:\X9) - runs last, rename needs restart
# -----------------------------------------------------------------------

View file

@ -56,11 +56,8 @@
;;
[General]
; #223B47 in COLORREF (0x00BBGGRR): R=34 G=59 B=71 = 71*65536 + 59*256 + 34 = 4668194
BackgroundColor = 4668194
; AutoBackground=0: do NOT read live desktop color - unreliable during deployment
; (live session may not yet reflect the registry background color change)
AutoBackground = 0
BackgroundColor = 2097152
AutoBackground = 1
Output = %temp%\backinfo.bmp
UpdateDesktop = 1
LineSpacing = 2

View file

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="amd64"
name="xetup"
type="win32"
/>
<description>xetup Windows deployment tool</description>
<!-- Request administrator privileges -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<!-- Enable modern ComCtl32 v6 (themed controls, proper checkboxes, etc.) -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<!-- DPI awareness: system-DPI aware (no blurry scaling) -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View file

@ -1,75 +0,0 @@
// Command xetup is the GUI launcher for Windows deployment.
//
// All PowerShell scripts and assets are embedded in the binary and extracted
// to a temp directory at runtime. The GUI collects configuration, streams
// live log output while the scripts run, and shows a final summary.
//
// Cross-compile for Windows (requires MinGW):
//
// CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
// GOOS=windows GOARCH=amd64 \
// go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup
package main
import (
"fmt"
"log"
"os"
"path/filepath"
xetupembed "git.xetup.x9.cz/x9/xetup"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/gui"
"git.xetup.x9.cz/x9/xetup/internal/runner"
"git.xetup.x9.cz/x9/xetup/internal/state"
)
func main() {
// Temp working directory cleaned up on exit
tmpDir, err := os.MkdirTemp("", "xetup-*")
if err != nil {
log.Fatalf("Cannot create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Extract embedded scripts and assets (required in both initial and resume mode)
if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
log.Fatalf("Failed to extract scripts: %v", err)
}
if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
log.Fatalf("Failed to extract assets: %v", err)
}
// Determine config: use stored state in resume mode, load from file otherwise
cfgPath := config.ConfigPath()
var cfg config.Config
if st, err := state.Load(); err == nil {
// Resume mode: use the config that was active when the run started
cfg = st.Config
if st.ConfigPath != "" {
cfgPath = st.ConfigPath
}
} else {
// Normal mode: load config from disk (falls back to defaults when absent)
cfg, err = config.Load(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err)
}
}
// Write runtime config JSON so PowerShell scripts can read it
cfgRuntimePath, err := runner.WriteConfig(cfg, tmpDir)
if err != nil {
log.Fatalf("Failed to write runtime config: %v", err)
}
runCfg := runner.RunConfig{
ScriptsDir: filepath.Join(tmpDir, "scripts"),
ConfigPath: cfgRuntimePath,
LogFile: `C:\Windows\Setup\Scripts\Deploy.log`,
ProfileType: cfg.Deployment.ProfileType,
}
gui.Run(cfg, runCfg, cfgPath)
}

View file

@ -38,8 +38,6 @@ services:
image: code.forgejo.org/forgejo/runner:6.3.1
container_name: xetup-runner
restart: unless-stopped
entrypoint: ["/bin/sh", "-c", "forgejo-runner daemon --config /etc/runner/config.yml"]
user: "0:996" # root:docker - needed for /var/run/docker.sock access
depends_on:
- forgejo
environment:
@ -47,7 +45,6 @@ services:
volumes:
- runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
- ./runner-config.yml:/etc/runner/config.yml:ro
networks:
- xetup

View file

@ -1,16 +0,0 @@
// Package xetup exposes embedded PowerShell scripts and assets for use by
// cmd/xetup. Placing the embed declarations here (at the module root) gives
// the //go:embed directives a clear, stable relative path to the content.
package xetup
import "embed"
// Scripts holds all PowerShell scripts from the scripts/ directory.
//
//go:embed scripts
var Scripts embed.FS
// Assets holds all deployment assets from the assets/ directory.
//
//go:embed assets
var Assets embed.FS

11
go.mod
View file

@ -1,11 +0,0 @@
module git.xetup.x9.cz/x9/xetup
go 1.24.0
require github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
require (
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
golang.org/x/sys v0.36.0 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
)

9
go.sum
View file

@ -1,9 +0,0 @@
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=

37
heal.sh
View file

@ -1,37 +0,0 @@
#!/bin/bash
# Self-heal: pull latest git changes, deploy web content, keep containers up.
# Runs every 5 minutes via systemd timer.
set -euo pipefail
cd /opt/xetup
LOG="[heal $(date '+%H:%M:%S')]"
# Pull latest changes (ff-only: never merge, just update)
BEFORE=$(git rev-parse HEAD 2>/dev/null || echo "none")
git fetch --quiet origin main 2>/dev/null || true
REMOTE=$(git rev-parse origin/main 2>/dev/null || echo "")
if [ -n "$REMOTE" ] && [ "$BEFORE" != "$REMOTE" ]; then
git reset --hard origin/main --quiet 2>/dev/null || true
AFTER=$(git rev-parse HEAD)
echo "$LOG deployed $BEFORE$AFTER"
# Write deployed commit info for the web (spec page footer)
echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
> web/data/deploy.json
# If web content changed, reload nginx config
if git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null | grep -q '^web/'; then
echo "$LOG web/ changed - reloading nginx"
/usr/bin/docker compose exec -T web nginx -s reload 2>/dev/null || true
fi
else
# Always keep deploy.json fresh on first run if missing
if [ ! -f web/data/deploy.json ]; then
echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
> web/data/deploy.json
fi
fi
# Ensure all containers are running
/usr/bin/docker compose up -d --remove-orphans 2>&1 | grep -v "up-to-date\|unchanged" || true

View file

@ -1,146 +0,0 @@
package config
import (
"encoding/json"
"os"
"path/filepath"
)
// Config mirrors config.json structure.
type Config struct {
Deployment Deployment `json:"deployment"`
AdminAccount AdminAccount `json:"adminAccount"`
Activation Activation `json:"activation"`
Software Software `json:"software"`
Steps map[string]bool `json:"steps"`
Features Features `json:"features"`
}
type Deployment struct {
PCName string `json:"pcName"`
PCDescription string `json:"pcDescription"`
Timezone string `json:"timezone"`
ProfileType string `json:"profileType"` // default | admin | user
}
type AdminAccount struct {
Username string `json:"username"`
}
type Activation struct {
ProductKey string `json:"productKey"`
KMSServer string `json:"kmsServer"`
}
type SoftwareItem struct {
Name string `json:"name"`
WingetID string `json:"wingetId"`
}
type Software struct {
Install []SoftwareItem `json:"install"`
}
// Features holds per-step, per-feature toggle flags.
// Keys: stepID -> featureID -> enabled.
// A missing key defaults to true (feature enabled).
type Features map[string]map[string]bool
// DefaultConfig returns a config with sensible defaults.
func DefaultConfig() Config {
return Config{
Deployment: Deployment{
Timezone: "Central Europe Standard Time",
ProfileType: "default",
},
AdminAccount: AdminAccount{
Username: "adminx9",
},
Activation: Activation{
ProductKey: "",
},
Software: Software{
Install: []SoftwareItem{
{Name: "7-Zip", WingetID: "7zip.7zip"},
{Name: "Adobe Acrobat Reader 64-bit", WingetID: "Adobe.Acrobat.Reader.64-bit"},
{Name: "OpenVPN Connect", WingetID: "OpenVPNTechnologies.OpenVPNConnect"},
},
},
Steps: map[string]bool{
"adminAccount": true,
"bloatware": true,
"software": true,
"systemRegistry": true,
"defaultProfile": true,
"personalization": true,
"scheduledTasks": true,
"backinfo": true,
"activation": true,
"dellUpdate": true,
"windowsUpdate": true,
"network": true,
"pcIdentity": true,
},
Features: Features{
"software": {
"wingetInstalls": true,
"pdfDefault": true,
"ateraAgent": true,
},
"systemRegistry": {
"systemTweaks": true,
"edgePolicies": true,
"oneDriveUninstall": true,
"powercfg": true,
"proxyDisable": true,
},
"defaultProfile": {
"taskbarTweaks": true,
"startMenuTweaks": true,
"explorerTweaks": true,
},
"dellUpdate": {
"drivers": true,
"bios": true,
},
},
}
}
// Load reads config.json from the given path.
// If the file does not exist, returns DefaultConfig without error.
func Load(path string) (Config, error) {
cfg := DefaultConfig()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return cfg, nil
}
if err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
// Save writes config to the given path (creates directories if needed).
func Save(cfg Config, path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// ConfigPath returns the default config.json path (next to the executable).
func ConfigPath() string {
exe, err := os.Executable()
if err != nil {
return "config.json"
}
return filepath.Join(filepath.Dir(exe), "config.json")
}

View file

@ -1,739 +0,0 @@
//go:build windows
// Package gui implements the Walk-based graphical interface for xetup.
//
// Three sequential phases, each with its own window:
// 1. Config form PC name, product key, profile, step checkboxes,
// load/save config buttons for per-client presets
// 2. Live run real-time log streamed from PowerShell scripts
// 3. Summary per-step OK / ERROR / SKIPPED with elapsed time
//
// Resume mode (state file present): skips the config form and runs only
// the pending steps recorded in the state file. On completion, disables
// autologon and removes the X9-Resume scheduled task.
package gui
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/prereboot"
"git.xetup.x9.cz/x9/xetup/internal/runner"
"git.xetup.x9.cz/x9/xetup/internal/state"
)
// Run opens the xetup window and blocks until the user closes it.
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
// Resume mode: state file present from a previous interrupted run
if st, err := state.Load(); err == nil {
resumePhase(st, runCfg)
return
}
// Normal flow: config form → run → summary
for {
res := formPhase(cfg, runCfg, cfgPath)
switch res.action {
case "quit":
return
case "reload":
cfg = res.cfg
cfgPath = res.cfgPath
case "run":
runCfg.ProfileType = res.cfg.Deployment.ProfileType
results, needsReboot := runPhase(runCfg, res.steps, false)
if needsReboot {
prepareRebootAndRestart(res.cfg, res.steps, results, cfgPath, runCfg)
return
}
donePhase(results, nil)
return
}
}
}
// --------------------------------------------------------------------------
// Resume mode
// --------------------------------------------------------------------------
func resumePhase(st *state.State, runCfg runner.RunConfig) {
runCfg.LogFile = st.LogFile
steps := runner.StepsByIDs(st.PendingSteps)
results, needsReboot := runPhase(runCfg, steps, true)
// Accumulate completed results (NeedsReboot step excluded runs again)
newResults := append(st.Results, toStateResults(results)...) //nolint:gocritic
if needsReboot {
// Update state and reboot infrastructure already in place (running as adminx9)
pending := pendingStepIDs(steps, results)
newSt := &state.State{
Config: st.Config,
ConfigPath: st.ConfigPath,
LogFile: st.LogFile,
PendingSteps: pending,
Results: newResults,
}
if err := state.Save(newSt); err != nil {
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
}
reboot()
return
}
// All steps complete
prereboot.Cleanup()
_ = state.Delete()
donePhase(results, st.Results)
}
// --------------------------------------------------------------------------
// Phase 1 Config form
// --------------------------------------------------------------------------
type formResult struct {
action string // "run", "reload", "quit"
cfg config.Config
cfgPath string
steps []runner.Step
}
func formPhase(cfg config.Config, runCfg runner.RunConfig, cfgPath string) formResult {
var mw *walk.MainWindow
result := formResult{action: "quit", cfgPath: cfgPath}
var (
pcNameLE *walk.LineEdit
pcDescLE *walk.LineEdit
productKeyLE *walk.LineEdit
profileCB *walk.ComboBox
)
items := runner.AllSelectableItems()
checkPtrs := make([]*walk.CheckBox, len(items))
checkWidgets := make([]Widget, len(items))
for i, item := range items {
i := i // capture for closure
checkWidgets[i] = CheckBox{
AssignTo: &checkPtrs[i],
Text: item.Label,
Checked: itemEnabled(cfg, item),
}
}
collectCfg := func() config.Config {
out := cfg
out.Deployment.PCName = pcNameLE.Text()
out.Deployment.PCDescription = pcDescLE.Text()
out.Activation.ProductKey = productKeyLE.Text()
profiles := []string{"default", "admin", "user"}
if idx := profileCB.CurrentIndex(); idx >= 0 && idx < len(profiles) {
out.Deployment.ProfileType = profiles[idx]
}
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checkPtrs[i].Checked()
}
_, features := buildStepsAndFeatures(selected)
out.Features = features
return out
}
if err := (MainWindow{
AssignTo: &mw,
Title: "xetup \u2014 Windows deployment",
Size: Size{Width: 760, Height: 740},
Layout: VBox{},
Children: []Widget{
// ── Form fields ──────────────────────────────────────────────────
Composite{
Layout: Grid{Columns: 2},
Children: []Widget{
Label{Text: "PC jmeno:"},
LineEdit{
AssignTo: &pcNameLE,
Text: cfg.Deployment.PCName,
CueBanner: "napr. NB-KLIENT-01 (prazdne = neprejmenovat)",
},
Label{Text: "Popis PC:"},
LineEdit{
AssignTo: &pcDescLE,
Text: cfg.Deployment.PCDescription,
CueBanner: "napr. PC recepce",
},
Label{Text: "Product Key:"},
LineEdit{
AssignTo: &productKeyLE,
Text: cfg.Activation.ProductKey,
CueBanner: "prazdne = OA3 / GVLK fallback",
},
Label{Text: "Profil:"},
ComboBox{
AssignTo: &profileCB,
Model: []string{"default", "admin", "user"},
CurrentIndex: profileIndex(cfg.Deployment.ProfileType),
},
},
},
HSeparator{},
// ── Step checkboxes ──────────────────────────────────────────────
Label{Text: "Kroky a nastaveni (odskrtnete co nechcete spustit):"},
ScrollView{
MinSize: Size{Height: 400},
Layout: VBox{MarginsZero: true},
Children: checkWidgets,
},
HSeparator{},
// ── Buttons ──────────────────────────────────────────────────────
Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
PushButton{
Text: "Nacist config...",
OnClicked: func() {
dlg := new(walk.FileDialog)
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
dlg.Title = "Nacist konfiguraci"
ok, err := dlg.ShowOpen(mw)
if err != nil || !ok {
return
}
data, err := os.ReadFile(dlg.FilePath)
if err != nil {
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
return
}
newCfg := config.DefaultConfig()
if err := json.Unmarshal(data, &newCfg); err != nil {
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
return
}
result = formResult{
action: "reload",
cfg: newCfg,
cfgPath: dlg.FilePath,
}
mw.Close()
},
},
PushButton{
Text: "Ulozit config...",
OnClicked: func() {
dlg := new(walk.FileDialog)
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
dlg.Title = "Ulozit konfiguraci"
dlg.FilePath = "config.json"
ok, err := dlg.ShowSave(mw)
if err != nil || !ok {
return
}
data, err := json.MarshalIndent(collectCfg(), "", " ")
if err != nil {
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
return
}
if err := os.WriteFile(dlg.FilePath, data, 0644); err != nil {
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
}
},
},
HSpacer{},
PushButton{
Text: " SPUSTIT ",
OnClicked: func() {
finalCfg := collectCfg()
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checkPtrs[i].Checked()
}
steps, features := buildStepsAndFeatures(selected)
finalCfg.Features = features
_ = config.Save(finalCfg, cfgPath)
result = formResult{
action: "run",
cfg: finalCfg,
cfgPath: cfgPath,
steps: steps,
}
mw.Close()
},
},
},
},
},
}.Create()); err != nil {
walk.MsgBox(nil, "Chyba", fmt.Sprintf("Nelze vytvorit okno: %v", err), walk.MsgBoxIconError)
return result
}
mw.Run()
return result
}
// --------------------------------------------------------------------------
// Phase 2 Live run view
// --------------------------------------------------------------------------
// stepIndicator tracks a step's label and current status.
type stepIndicator struct {
label *walk.Label
stepID string
status string // "pending", "running", "ok", "error", "skipped", "cancelled"
}
// runPhase runs steps and streams live log output.
// resuming=true changes the title and initial status message.
// Returns (results, needsReboot).
func runPhase(runCfg runner.RunConfig, steps []runner.Step, resuming bool) ([]runner.Result, bool) {
var (
mw *walk.MainWindow
statusLbl *walk.Label
logTE *walk.TextEdit
cancelFn context.CancelFunc
)
// Build step indicator labels (one per step, in order)
indicators := make([]stepIndicator, len(steps))
stepIndex := make(map[string]int, len(steps))
indWidgets := make([]Widget, len(steps))
for i, s := range steps {
indicators[i] = stepIndicator{stepID: s.ID, status: "pending"}
stepIndex[s.ID] = i
indWidgets[i] = Label{
AssignTo: &indicators[i].label,
Text: s.Num + " \u00b7",
Font: Font{Family: "Consolas", PointSize: 9},
MinSize: Size{Width: 38},
}
}
title := "xetup \u2014 probiha instalace"
initialStatus := "Spoustim..."
if resuming {
title = "xetup \u2014 pokracuji po restartu"
initialStatus = "Pokracuji po restartu..."
}
if err := (MainWindow{
AssignTo: &mw,
Title: title,
Size: Size{Width: 760, Height: 740},
Layout: VBox{},
Children: []Widget{
Label{AssignTo: &statusLbl, Text: initialStatus},
// Step progress strip
Composite{
Layout: HBox{MarginsZero: true},
Children: indWidgets,
},
HSeparator{},
TextEdit{
AssignTo: &logTE,
ReadOnly: true,
VScroll: true,
Font: Font{Family: "Consolas", PointSize: 9},
MinSize: Size{Height: 530},
},
HSeparator{},
Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
HSpacer{},
PushButton{
Text: " ZASTAVIT ",
OnClicked: func() {
if cancelFn != nil {
cancelFn()
}
},
},
},
},
},
}.Create()); err != nil {
return nil, false
}
// updateIndicator refreshes a step label's text and color — call inside Synchronize.
updateIndicator := func(idx int) {
ind := &indicators[idx]
s := steps[idx]
switch ind.status {
case "running":
ind.label.SetText(s.Num + " \u25ba") // ►
ind.label.SetTextColor(walk.RGB(30, 144, 255))
case "ok":
ind.label.SetText(s.Num + " \u2713") // ✓
ind.label.SetTextColor(walk.RGB(0, 180, 0))
case "error":
ind.label.SetText(s.Num + " \u2717") // ✗
ind.label.SetTextColor(walk.RGB(220, 50, 50))
case "skipped", "cancelled":
ind.label.SetText(s.Num + " \u2013") //
ind.label.SetTextColor(walk.RGB(140, 140, 140))
default:
ind.label.SetText(s.Num + " \u00b7") // ·
ind.label.SetTextColor(walk.RGB(180, 180, 180))
}
}
var (
mu sync.Mutex
results []runner.Result
done = make(chan struct{})
)
ctx, cancel := context.WithCancel(context.Background())
cancelFn = cancel
mw.Closing().Attach(func(_ *bool, _ walk.CloseReason) {
cancelFn()
})
r := runner.New(
runCfg,
func(l runner.LogLine) {
mw.Synchronize(func() {
logTE.AppendText(l.Text + "\r\n")
})
},
func(step runner.Step) {
mw.Synchronize(func() {
statusLbl.SetText(fmt.Sprintf(
"Krok %s \u2013 %s...", step.Num, step.Name,
))
if idx, ok := stepIndex[step.ID]; ok {
indicators[idx].status = "running"
updateIndicator(idx)
}
})
},
func(res runner.Result) {
mw.Synchronize(func() {
statusLbl.SetText(fmt.Sprintf(
"Krok %s \u2013 %s: %s", res.Step.Num, res.Step.Name, res.Status,
))
if idx, ok := stepIndex[res.Step.ID]; ok {
indicators[idx].status = strings.ToLower(res.Status)
updateIndicator(idx)
}
})
},
)
go func() {
defer close(done)
res := r.Run(ctx, steps)
mu.Lock()
results = res
mu.Unlock()
mw.Synchronize(func() {
mw.Close()
})
}()
mw.Run()
cancel()
<-done
mu.Lock()
defer mu.Unlock()
needsReboot := len(results) > 0 && results[len(results)-1].NeedsReboot
return results, needsReboot
}
// --------------------------------------------------------------------------
// Pre-reboot helper
// --------------------------------------------------------------------------
// prepareRebootAndRestart saves state, sets up resume infrastructure, and reboots.
func prepareRebootAndRestart(
cfg config.Config,
allSteps []runner.Step,
completedResults []runner.Result,
cfgPath string,
runCfg runner.RunConfig,
) {
pending := pendingStepIDs(allSteps, completedResults)
st := &state.State{
Config: cfg,
ConfigPath: cfgPath,
LogFile: runCfg.LogFile,
PendingSteps: pending,
Results: toStateResults(completedResults),
}
if err := state.Save(st); err != nil {
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
return
}
if err := prereboot.Prepare(); err != nil {
walk.MsgBox(nil, "Chyba", "Prereboot setup selhal: "+err.Error(), walk.MsgBoxIconError)
return
}
reboot()
}
// pendingStepIDs returns IDs of steps that still need to run.
// Steps with NeedsReboot=true (re-run after reboot) and steps not yet
// reached (absent from completedResults) are both included.
func pendingStepIDs(allSteps []runner.Step, completedResults []runner.Result) []string {
done := make(map[string]bool, len(completedResults))
for _, r := range completedResults {
if !r.NeedsReboot {
done[r.Step.ID] = true
}
}
var pending []string
for _, s := range allSteps {
if !done[s.ID] {
pending = append(pending, s.ID)
}
}
return pending
}
// toStateResults converts runner results to state.StepResult for persistence.
// Steps with NeedsReboot=true are excluded (they will run again next round).
func toStateResults(results []runner.Result) []state.StepResult {
var sr []state.StepResult
for _, r := range results {
if r.NeedsReboot {
continue
}
sr = append(sr, state.StepResult{
StepID: r.Step.ID,
Num: r.Step.Num,
Name: r.Step.Name,
Status: r.Status,
Elapsed: r.Elapsed,
})
}
return sr
}
// --------------------------------------------------------------------------
// Phase 3 Summary with auto-reboot countdown
// --------------------------------------------------------------------------
const rebootCountdown = 60 // seconds before automatic reboot
// donePhase shows the final summary screen.
// currentResults are from the last run; prevResults accumulate earlier rounds.
func donePhase(currentResults []runner.Result, prevResults []state.StepResult) {
var (
mw *walk.MainWindow
countdownLbl *walk.Label
)
// Combined ordered list: previous rounds first, then current round
type displayRow struct {
Num string
Name string
Status string
Elapsed time.Duration
}
var rows []displayRow
for _, r := range prevResults {
rows = append(rows, displayRow{r.Num, r.Name, r.Status, r.Elapsed})
}
for _, r := range currentResults {
if r.NeedsReboot {
continue
}
rows = append(rows, displayRow{r.Step.Num, r.Step.Name, r.Status, r.Elapsed})
}
ok, errs, skipped := 0, 0, 0
widgets := make([]Widget, 0, len(rows))
for _, row := range rows {
var icon string
switch row.Status {
case "OK":
ok++
icon = "OK "
case "ERROR":
errs++
icon = "ERR "
default:
skipped++
icon = "\u2013 "
}
text := icon + row.Num + " \u2013 " + row.Name
if row.Elapsed > 0 {
text += fmt.Sprintf(" (%s)", row.Elapsed.Round(time.Second))
}
widgets = append(widgets, Label{
Text: text,
Font: Font{Family: "Consolas", PointSize: 9},
})
}
summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped)
cancelReboot := make(chan struct{})
if err := (MainWindow{
AssignTo: &mw,
Title: "xetup \u2014 hotovo",
Size: Size{Width: 760, Height: 740},
Layout: VBox{},
Children: []Widget{
Label{
Text: "Hotovo",
Alignment: AlignHCenterVNear,
Font: Font{Bold: true, PointSize: 12},
},
HSeparator{},
ScrollView{
MinSize: Size{Height: 510},
Layout: VBox{MarginsZero: true},
Children: widgets,
},
HSeparator{},
Label{
Text: summaryText,
Alignment: AlignHCenterVNear,
Font: Font{Bold: true},
},
Label{
AssignTo: &countdownLbl,
Text: fmt.Sprintf("Restart za %ds...", rebootCountdown),
Alignment: AlignHCenterVNear,
},
Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
HSpacer{},
PushButton{
Text: " Restartovat ted ",
OnClicked: func() {
close(cancelReboot)
reboot()
mw.Close()
},
},
PushButton{
Text: " Zrusit restart ",
OnClicked: func() {
select {
case <-cancelReboot:
default:
close(cancelReboot)
}
countdownLbl.SetText("Restart zrusen.")
},
},
HSpacer{},
},
},
},
}.Create()); err != nil {
return
}
// Countdown goroutine
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
remaining := rebootCountdown
for {
select {
case <-cancelReboot:
return
case <-ticker.C:
remaining--
r := remaining
mw.Synchronize(func() {
if r > 0 {
countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
} else {
countdownLbl.SetText("Restartuji...")
}
})
if remaining <= 0 {
reboot()
mw.Synchronize(func() { mw.Close() })
return
}
}
}
}()
mw.Run()
}
// reboot issues an immediate Windows restart.
func reboot() {
exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
if item.FeatureID == "" {
if v, ok := cfg.Steps[item.StepID]; ok {
return v
}
return true
}
if feats, ok := cfg.Features[item.StepID]; ok {
if v, ok2 := feats[item.FeatureID]; ok2 {
return v
}
}
return true
}
func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
items := runner.AllSelectableItems()
features := make(config.Features)
stepOn := make(map[string]bool)
for _, item := range items {
if item.FeatureID == "" {
stepOn[item.StepID] = selected[item.Key]
} else {
if features[item.StepID] == nil {
features[item.StepID] = make(map[string]bool)
}
features[item.StepID][item.FeatureID] = selected[item.Key]
if selected[item.Key] {
stepOn[item.StepID] = true
}
}
}
allSteps := runner.AllSteps()
steps := make([]runner.Step, len(allSteps))
for i, s := range allSteps {
s.Enabled = stepOn[s.ID]
steps[i] = s
}
return steps, features
}
func profileIndex(p string) int {
switch p {
case "admin":
return 1
case "user":
return 2
default:
return 0
}
}

View file

@ -1,20 +0,0 @@
//go:build !windows
// Package prereboot ensures the reboot-resume infrastructure is in place
// before xetup triggers a system restart mid-deployment.
package prereboot
// StablePath is the platform stub for the stable binary path.
const StablePath = "/tmp/xetup-resume"
// TaskName is the scheduled task that re-launches xetup after each reboot.
const TaskName = "X9-Resume"
// IsAdminx9 always returns false on non-Windows platforms.
func IsAdminx9() bool { return false }
// Prepare is a no-op on non-Windows platforms.
func Prepare() error { return nil }
// Cleanup is a no-op on non-Windows platforms.
func Cleanup() {}

View file

@ -1,190 +0,0 @@
//go:build windows
// Package prereboot ensures the reboot-resume infrastructure is in place
// before xetup triggers a system restart mid-deployment.
package prereboot
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"golang.org/x/sys/windows/registry"
)
// StablePath is where xetup.exe is copied so it survives across reboots.
const StablePath = `C:\Windows\Setup\Scripts\xetup.exe`
// TaskName is the scheduled task that re-launches xetup after each reboot.
const TaskName = "X9-Resume"
const adminUser = "adminx9"
const winlogonKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`
// IsAdminx9 reports whether the current process runs as the adminx9 account.
func IsAdminx9() bool {
return strings.EqualFold(os.Getenv("USERNAME"), adminUser)
}
// Prepare ensures the reboot-resume infrastructure is in place.
// When not running as adminx9 (first reboot trigger):
// - creates the adminx9 local account if it does not exist
// - copies the running binary to StablePath
// - enables autologon for adminx9 (AutoLogonCount=20 safety cap)
// - registers the X9-Resume scheduled task (AtLogOn adminx9, RunLevel Highest)
//
// When already running as adminx9 (subsequent rounds) all of the above is
// already in place; the function returns immediately without changes.
// Safe to call repeatedly (idempotent).
func Prepare() error {
if IsAdminx9() {
return nil
}
if err := ensureAdminx9User(); err != nil {
return fmt.Errorf("adminx9 account: %w", err)
}
if err := copySelfTo(StablePath); err != nil {
return fmt.Errorf("copy binary: %w", err)
}
if err := setAutologon(); err != nil {
return fmt.Errorf("autologon: %w", err)
}
if err := registerResumeTask(); err != nil {
return fmt.Errorf("resume task: %w", err)
}
return nil
}
// Cleanup disables autologon and removes the X9-Resume scheduled task.
// Called when all deployment steps have completed successfully.
func Cleanup() {
_ = disableAutologon()
_ = unregisterResumeTask()
}
// ensureAdminx9User creates the adminx9 local account if absent.
func ensureAdminx9User() error {
// "net user adminx9" exits 0 when user exists
if err := newHiddenCmd("net", "user", adminUser).Run(); err == nil {
return nil // already exists
}
// Create with empty password
if b, err := newHiddenCmd("net", "user", adminUser, "", "/add").CombinedOutput(); err != nil {
return fmt.Errorf("net user /add: %s: %w", strings.TrimSpace(string(b)), err)
}
// Add to local Administrators group
if b, err := newHiddenCmd("net", "localgroup", "Administrators", adminUser, "/add").CombinedOutput(); err != nil {
return fmt.Errorf("net localgroup: %s: %w", strings.TrimSpace(string(b)), err)
}
// Hide from the Windows login screen
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList`,
registry.SET_VALUE)
if err != nil {
return fmt.Errorf("registry hide: %w", err)
}
defer k.Close()
return k.SetDWordValue(adminUser, 0)
}
// copySelfTo copies the running executable to dst.
// Skips the copy when src and dst resolve to the same path.
func copySelfTo(dst string) error {
src, err := os.Executable()
if err != nil {
return err
}
if resolved, err := filepath.EvalSymlinks(src); err == nil {
src = resolved
}
if strings.EqualFold(src, dst) {
return nil
}
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
return copyFile(src, dst)
}
func copyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, r)
return err
}
func setAutologon() error {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
for _, kv := range []struct{ name, val string }{
{"AutoAdminLogon", "1"},
{"DefaultUserName", adminUser},
{"DefaultPassword", ""},
{"DefaultDomainName", "."},
} {
if err := k.SetStringValue(kv.name, kv.val); err != nil {
return fmt.Errorf("set %s: %w", kv.name, err)
}
}
// Safety cap: self-limits even if cleanup task fails to run
return k.SetDWordValue("AutoLogonCount", 20)
}
func disableAutologon() error {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
_ = k.SetStringValue("AutoAdminLogon", "0")
_ = k.DeleteValue("DefaultPassword")
_ = k.DeleteValue("AutoLogonCount")
return nil
}
func registerResumeTask() error {
ps := fmt.Sprintf(`
$action = New-ScheduledTaskAction -Execute '%s' -Argument '--resume'
$trigger = New-ScheduledTaskTrigger -AtLogOn -User '%s'
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 4) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId '%s' -LogonType Interactive -RunLevel Highest
Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName 'X9-Resume' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
`, StablePath, adminUser, adminUser)
return runPS(ps)
}
func unregisterResumeTask() error {
return runPS(`Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue`)
}
func runPS(script string) error {
cmd := newHiddenCmd("powershell.exe",
"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
b, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(b)), err)
}
return nil
}
func newHiddenCmd(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd
}

View file

@ -1,7 +0,0 @@
//go:build !windows
package runner
import "os/exec"
func hideWindow(cmd *exec.Cmd) {}

View file

@ -1,14 +0,0 @@
//go:build windows
package runner
import (
"os/exec"
"syscall"
)
// hideWindow prevents the child process from creating a visible console window.
// Without this, powershell.exe opens a full-size window on top of the GUI.
func hideWindow(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}

View file

@ -1,417 +0,0 @@
// Package runner executes PowerShell deployment scripts and streams log output.
package runner
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
)
// ErrNeedsReboot is returned by runScript when a PowerShell script exits with
// code 9, signalling that it completed successfully but requires a system
// reboot before deployment can continue.
var ErrNeedsReboot = errors.New("reboot required")
// Step describes a single deployment step.
type Step struct {
ID string // e.g. "adminAccount"
Num string // display number e.g. "00"
Name string
ScriptName string // e.g. "00-admin-account.ps1"
Enabled bool
}
// AllSteps returns the ordered list of deployment steps.
func AllSteps() []Step {
return []Step{
{ID: "adminAccount", Num: "00", Name: "Admin ucet", ScriptName: "00-admin-account.ps1"},
{ID: "bloatware", Num: "01", Name: "Bloatware removal", ScriptName: "01-bloatware.ps1"},
{ID: "software", Num: "02", Name: "Software (winget)", ScriptName: "02-software.ps1"},
{ID: "systemRegistry", Num: "03", Name: "System Registry (HKLM)", ScriptName: "03-system-registry.ps1"},
{ID: "defaultProfile", Num: "04", Name: "Default Profile", ScriptName: "04-default-profile.ps1"},
{ID: "personalization", Num: "05", Name: "Personalizace", ScriptName: "05-personalization.ps1"},
{ID: "scheduledTasks", Num: "06", Name: "Scheduled Tasks", ScriptName: "06-scheduled-tasks.ps1"},
{ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"},
{ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"},
{ID: "pcIdentity", Num: "09", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
{ID: "network", Num: "10", Name: "Network discovery", ScriptName: "10-network.ps1"},
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
{ID: "windowsUpdate", Num: "12", Name: "Windows Update", ScriptName: "12-windows-update.ps1"},
}
}
// Feature is a single toggleable sub-item within a deployment step.
type Feature struct {
ID string
Label string
}
// StepFeatures returns per-step feature lists. Steps absent from this map
// have no sub-features and are controlled at the step level only.
func StepFeatures() map[string][]Feature {
return map[string][]Feature{
"software": {
{ID: "wingetInstalls", Label: "Instalace SW ze seznamu (winget)"},
{ID: "pdfDefault", Label: "Adobe Reader jako vychozi PDF"},
{ID: "ateraAgent", Label: "Atera RMM agent"},
},
"systemRegistry": {
{ID: "systemTweaks", Label: "Windows tweaky (Widgets, GameDVR, Recall...)"},
{ID: "edgePolicies", Label: "Edge policies (tlacitka, vyhledavac, telemetrie)"},
{ID: "oneDriveUninstall", Label: "OneDrive uninstall (consumer pre-install)"},
{ID: "powercfg", Label: "Nastaveni napajeni (timeout AC/DC)"},
{ID: "proxyDisable", Label: "Zakaz WPAD proxy auto-detect"},
},
"defaultProfile": {
{ID: "taskbarTweaks", Label: "Taskbar zarovnani, tlacitka, layout XML"},
{ID: "startMenuTweaks", Label: "Start menu cisteni pinu, Bing, Copilot"},
{ID: "explorerTweaks", Label: "Explorer pripony, LaunchTo, ShowRecent"},
},
"dellUpdate": {
{ID: "drivers", Label: "Dell drivery + firmware"},
{ID: "bios", Label: "Dell BIOS update"},
},
}
}
// SelectableItem is a single toggleable row in the TUI checklist.
// It represents either a whole step (FeatureID == "") or a specific feature.
type SelectableItem struct {
Key string // "stepID" or "stepID.featureID"
StepID string
FeatureID string // empty for step-level items
Label string
Num string
}
// AllSelectableItems returns the flat ordered list of all TUI toggle rows.
// Steps with features are expanded to individual feature rows.
// Steps without features appear as a single step-level row.
func AllSelectableItems() []SelectableItem {
steps := AllSteps()
features := StepFeatures()
var items []SelectableItem
for _, s := range steps {
feats, hasFeatures := features[s.ID]
if !hasFeatures {
items = append(items, SelectableItem{
Key: s.ID,
StepID: s.ID,
Label: s.Num + " " + s.Name,
Num: s.Num,
})
} else {
for _, f := range feats {
items = append(items, SelectableItem{
Key: s.ID + "." + f.ID,
StepID: s.ID,
FeatureID: f.ID,
Label: s.Num + " " + f.Label,
Num: s.Num,
})
}
}
}
return items
}
// RunConfig holds runtime parameters passed to each script.
type RunConfig struct {
ScriptsDir string
ConfigPath string
LogFile string
ProfileType string
}
// Result is the outcome of a single step.
type Result struct {
Step Step
Status string // "OK", "ERROR", "SKIPPED", "CANCELLED"
Elapsed time.Duration
NeedsReboot bool // true when the script exited with code 9
}
// LogLine is a single output line from a running script.
type LogLine struct {
StepID string
Text string
Level string // INFO, OK, ERROR, WARN, STEP - parsed from [LEVEL] prefix
}
// Runner executes deployment steps sequentially.
type Runner struct {
cfg RunConfig
onLog func(LogLine)
onStepStart func(Step)
onResult func(Result)
cancel context.CancelFunc
}
// New creates a Runner. onLog is called for each output line, onResult after each step.
// onStepStart (optional) is called immediately before a step's script is launched.
func New(cfg RunConfig, onLog func(LogLine), onStepStart func(Step), onResult func(Result)) *Runner {
return &Runner{cfg: cfg, onLog: onLog, onStepStart: onStepStart, onResult: onResult}
}
// Run executes enabled steps sequentially. Blocks until done or context cancelled.
func (r *Runner) Run(ctx context.Context, steps []Step) []Result {
ctx, cancel := context.WithCancel(ctx)
r.cancel = cancel
defer cancel()
// Write config JSON to temp file so scripts can read it
cfgArg := r.cfg.ConfigPath
var results []Result
for _, step := range steps {
if !step.Enabled {
res := Result{Step: step, Status: "SKIPPED"}
r.onResult(res)
results = append(results, res)
continue
}
if r.onStepStart != nil {
r.onStepStart(step)
}
start := time.Now()
err := r.runScript(ctx, step, cfgArg)
elapsed := time.Since(start)
status := "OK"
needsReboot := false
if err != nil {
if errors.Is(err, ErrNeedsReboot) {
needsReboot = true
// status remains "OK": step completed, reboot is just required to continue
} else if ctx.Err() != nil {
status = "CANCELLED"
} else {
status = "ERROR"
}
}
res := Result{Step: step, Status: status, Elapsed: elapsed, NeedsReboot: needsReboot}
r.onResult(res)
results = append(results, res)
if ctx.Err() != nil || needsReboot {
break
}
}
return results
}
// Stop cancels the running deployment.
func (r *Runner) Stop() {
if r.cancel != nil {
r.cancel()
}
}
func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error {
scriptPath := filepath.Join(r.cfg.ScriptsDir, step.ScriptName)
// Build argument list
args := []string{
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", scriptPath,
"-LogFile", r.cfg.LogFile,
}
// Pass config object as JSON string (script reads it inline)
if cfgArg != "" {
args = append(args, "-Config", fmt.Sprintf("(Get-Content '%s' | ConvertFrom-Json)", cfgArg))
}
// ProfileType for step 04
if step.ID == "defaultProfile" && r.cfg.ProfileType != "" {
args = append(args, "-ProfileType", r.cfg.ProfileType)
}
cmd := exec.CommandContext(ctx, "powershell.exe", args...)
hideWindow(cmd) // prevent PS console window from appearing over the GUI
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout // merge stderr into stdout
if err := cmd.Start(); err != nil {
return err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
if skipPSNoiseLine(line) {
continue
}
r.onLog(LogLine{
StepID: step.ID,
Text: line,
Level: parseLevel(line),
})
}
waitErr := cmd.Wait()
if waitErr != nil {
var exitErr *exec.ExitError
if errors.As(waitErr, &exitErr) && exitErr.ExitCode() == 9 {
return ErrNeedsReboot
}
}
return waitErr
}
// skipPSNoiseLine returns true for PowerShell stderr noise that clutters the log:
// multi-line error blocks (At line:N, CategoryInfo, FullyQualifiedErrorId, etc.),
// blank lines, and VERBOSE: prefix lines already handled by Write-Log.
func skipPSNoiseLine(line string) bool {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
return true
}
for _, prefix := range []string{
"At line:",
"+ CategoryInfo",
"+ FullyQualifiedErrorId",
"+ PositionMessage",
"VERBOSE:",
"DEBUG:",
} {
if strings.HasPrefix(trimmed, prefix) {
return true
}
}
// PS error continuation lines start with spaces + "+" or "~"
if len(trimmed) > 0 && (trimmed[0] == '+' || strings.HasPrefix(trimmed, "~")) {
return true
}
return false
}
// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
func parseLevel(line string) string {
if strings.Contains(line, "] [OK]") {
return "OK"
}
if strings.Contains(line, "] [ERROR]") {
return "ERROR"
}
if strings.Contains(line, "] [WARN]") {
return "WARN"
}
if strings.Contains(line, "] [STEP]") {
return "STEP"
}
return "INFO"
}
// ExtractScripts unpacks embedded scripts to a temp directory.
// Returns the directory path. Caller is responsible for cleanup.
func ExtractScripts(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
entries, err := fs.ReadDir("scripts")
if err != nil {
return fmt.Errorf("read embedded scripts: %w", err)
}
scriptsDir := filepath.Join(tmpDir, "scripts")
if err := os.MkdirAll(scriptsDir, 0755); err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue
}
// embed.FS always uses forward slashes regardless of OS
data, err := fs.ReadFile(path.Join("scripts", e.Name()))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(scriptsDir, e.Name()), data, 0644); err != nil {
return err
}
}
return nil
}
// ExtractAssets unpacks embedded assets to tmpDir/assets.
func ExtractAssets(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
return extractDir(fs, "assets", tmpDir)
}
func extractDir(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, src, dstBase string) error {
entries, err := fs.ReadDir(src)
if err != nil {
return err
}
dst := filepath.Join(dstBase, filepath.FromSlash(src))
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
for _, e := range entries {
// embed.FS always uses forward slashes regardless of OS
srcPath := path.Join(src, e.Name())
dstPath := filepath.Join(dstBase, filepath.FromSlash(srcPath))
if e.IsDir() {
if err := extractDir(fs, srcPath, dstBase); err != nil {
return err
}
continue
}
data, err := fs.ReadFile(srcPath)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
}
return nil
}
// StepsByIDs returns steps from AllSteps() whose IDs are in the given list,
// preserving the canonical AllSteps() order.
func StepsByIDs(ids []string) []Step {
want := make(map[string]bool, len(ids))
for _, id := range ids {
want[id] = true
}
var result []Step
for _, s := range AllSteps() {
if want[s.ID] {
result = append(result, s)
}
}
return result
}
// StepByID returns the Step with the given ID, and true if found.
func StepByID(id string) (Step, bool) {
for _, s := range AllSteps() {
if s.ID == id {
return s, true
}
}
return Step{}, false
}
// WriteConfig serialises cfg to a temp JSON file and returns its path.
func WriteConfig(cfg interface{}, tmpDir string) (string, error) {
path := filepath.Join(tmpDir, "config-runtime.json")
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return "", err
}
return path, os.WriteFile(path, data, 0644)
}

View file

@ -1,12 +0,0 @@
//go:build !windows
package state
import "os"
func statePath() string {
if h, err := os.UserHomeDir(); err == nil {
return h + "/xetup-state.json"
}
return "/tmp/xetup-state.json"
}

View file

@ -1,7 +0,0 @@
//go:build windows
package state
func statePath() string {
return `C:\Windows\Setup\Scripts\xetup-state.json`
}

View file

@ -1,74 +0,0 @@
// Package state manages the persistent xetup deployment state used to resume
// after a system reboot. The state file is written before each reboot and
// deleted once all deployment steps have completed.
package state
import (
"encoding/json"
"os"
"path/filepath"
"time"
"git.xetup.x9.cz/x9/xetup/internal/config"
)
// StepResult records the outcome of a single deployment step.
// Results are accumulated across reboot rounds so the final summary
// can show every step regardless of which round it ran in.
type StepResult struct {
StepID string `json:"stepID"`
Num string `json:"num"`
Name string `json:"name"`
Status string `json:"status"` // "OK", "ERROR", "SKIPPED"
Elapsed time.Duration `json:"elapsed"`
}
// State holds everything xetup needs to resume after a reboot.
type State struct {
Config config.Config `json:"config"`
ConfigPath string `json:"configPath"` // path to original config.json
LogFile string `json:"logFile"`
PendingSteps []string `json:"pendingSteps"` // step IDs to run next, in canonical order
Results []StepResult `json:"results"` // accumulated across all rounds
}
// Load reads the state file. Returns a non-nil error when the file is absent.
func Load() (*State, error) {
data, err := os.ReadFile(statePath())
if err != nil {
return nil, err
}
var s State
if err := json.Unmarshal(data, &s); err != nil {
return nil, err
}
return &s, nil
}
// Save writes the state file, creating parent directories as needed.
func Save(s *State) error {
p := statePath()
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(p, data, 0644)
}
// Delete removes the state file. Silently ignores not-found.
func Delete() error {
err := os.Remove(statePath())
if os.IsNotExist(err) {
return nil
}
return err
}
// Exists reports whether a state file is present on disk.
func Exists() bool {
_, err := os.Stat(statePath())
return err == nil
}

View file

@ -1,21 +0,0 @@
log:
level: info
runner:
file: /data/.runner
capacity: 1
timeout: 3h
fetch_timeout: 5s
fetch_interval: 2s
report_interval: 1s
cache:
enabled: true
container:
network: xetup
privileged: false
valid_volumes:
- '**'
docker_host: "-"
force_pull: false

View file

@ -27,7 +27,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -23,7 +23,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -28,22 +28,9 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Check winget availability
# -----------------------------------------------------------------------
@ -75,7 +62,6 @@ Write-Log "winget found: $($winget.Source -or $winget)" -Level OK
# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "wingetInstalls") {
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
Write-Log "No software list in config - skipping installs" -Level WARN
} else {
@ -100,14 +86,16 @@ if (Get-Feature $Config "software" "wingetInstalls") {
}
}
}
} else {
Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "pdfDefault") {
$forcePdf = $true
if ($Config -and $Config.pdfDefault) {
$forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
}
if ($forcePdf) {
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
# Stop UCPD driver before writing file association.
@ -142,11 +130,6 @@ if (Get-Feature $Config "software" "pdfDefault") {
} else {
Write-Log " Found: $acroExe" -Level INFO
# Mount HKCR PSDrive - not available by default in PS sessions
if (-not (Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue)) {
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null
}
# Set file type association via HKCR (system-wide, requires admin)
$progId = "AcroExch.Document.DC"
$openCmd = "`"$acroExe`" `"%1`""
@ -177,14 +160,11 @@ if (Get-Feature $Config "software" "pdfDefault") {
Write-Log " Could not restart UCPD: $_" -Level WARN
}
}
} else {
Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "ateraAgent") {
Write-Log "Installing Atera RMM Agent" -Level INFO
$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
@ -212,8 +192,5 @@ if (Get-Feature $Config "software" "ateraAgent") {
finally {
Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}
} else {
Write-Log "ateraAgent feature disabled - skipping" -Level INFO
}
Write-Log "Step 2 complete" -Level OK

View file

@ -37,22 +37,9 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
@ -219,108 +206,43 @@ function Remove-Reg {
Write-Log "3 - Applying HKLM system registry tweaks" -Level STEP
# -----------------------------------------------------------------------
# Always-on: password and timezone (fundamental system settings)
# -----------------------------------------------------------------------
Write-Log " Setting password max age to UNLIMITED" -Level INFO
$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " Password max age set to UNLIMITED" -Level OK
} else {
Write-Log " Failed to set password max age: $pwResult" -Level ERROR
}
$tz = "Central Europe Standard Time"
if ($Config -and $Config.deployment -and $Config.deployment.timezone) {
$tz = $Config.deployment.timezone
}
Write-Log " Setting time zone: $tz" -Level INFO
try {
Set-TimeZone -Id $tz -ErrorAction Stop
Write-Log " Time zone set: $tz" -Level OK
}
catch {
Write-Log " Failed to set time zone: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# System tweaks (Windows features, cloud noise, Xbox, Recall, Search UI)
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "systemTweaks") {
Write-Log " Applying system tweaks" -Level INFO
# Bypass Network Requirement on OOBE (BypassNRO)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" `
-Name "BypassNRO" -Value 1
# -----------------------------------------------------------------------
# Disable auto-install of Teams (Chat)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" `
-Name "ConfigureChatAutoInstall" -Value 0
# -----------------------------------------------------------------------
# Disable Cloud Optimized Content (ads in Start menu etc.)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableCloudOptimizedContent" -Value 1
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableWindowsConsumerFeatures" -Value 1
# -----------------------------------------------------------------------
# Disable Widgets (News and Interests)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" `
-Name "AllowNewsAndInterests" -Value 0
# Disable Outlook (new) auto-install via UScheduler
Write-Log " Disabling Outlook (new) auto-install" -Level INFO
$uschedulerPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate"
)
foreach ($uPath in $uschedulerPaths) {
if (Test-Path $uPath) {
try {
Remove-Item -Path $uPath -Recurse -Force
Write-Log " Removed UScheduler key: $uPath" -Level OK
}
catch {
Write-Log " Failed to remove UScheduler key: $_" -Level WARN
}
}
}
# Disable GameDVR
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
-Name "AllowGameDVR" -Value 0
# Disable Recall (Windows AI feature)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
-Name "DisableAIDataAnalysis" -Value 1
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
# this policy key ensures the setting survives Windows Updates.
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
-Name "SearchOnTaskbarMode" -Value 0
# Start menu - hide Recommended section (Win11)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" `
-Name "HideRecommendedSection" -Value 1
} else {
Write-Log "systemTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Microsoft Edge policies
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "edgePolicies") {
Write-Log " Applying Edge policies" -Level INFO
$edgePath = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
# UI / first run
Set-Reg -Path $edgePath -Name "HideFirstRunExperience" -Value 1
Set-Reg -Path $edgePath -Name "DefaultBrowserSettingEnabled" -Value 0
# New tab page - disable all visual clutter
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0 # feed / obsah
Set-Reg -Path $edgePath -Name "NewTabPageQuickLinksEnabled" -Value 0 # rychle odkazy
Set-Reg -Path $edgePath -Name "NewTabPageBackgroundEnabled" -Value 0 # pozadi
Set-Reg -Path $edgePath -Name "NewTabPageAllowedBackgroundTypes" -Value 3 # 3 = only solid color
# New tab page / recommendations
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SpotlightExperiencesAndRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "PersonalizationReportingEnabled" -Value 0
@ -366,8 +288,32 @@ if (Get-Feature $Config "systemRegistry" "edgePolicies") {
# Disable desktop shortcut on install/update
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" `
-Name "CreateDesktopShortcutDefault" -Value 0
# -----------------------------------------------------------------------
# Password - no expiration
# -----------------------------------------------------------------------
Write-Log " Setting password max age to UNLIMITED" -Level INFO
$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " Password max age set to UNLIMITED" -Level OK
} else {
Write-Log "edgePolicies feature disabled - skipping" -Level INFO
Write-Log " Failed to set password max age: $pwResult" -Level ERROR
}
# -----------------------------------------------------------------------
# Time zone
# -----------------------------------------------------------------------
$tz = "Central Europe Standard Time"
if ($Config -and $Config.deployment -and $Config.deployment.timezone) {
$tz = $Config.deployment.timezone
}
Write-Log " Setting time zone: $tz" -Level INFO
try {
Set-TimeZone -Id $tz -ErrorAction Stop
Write-Log " Time zone set: $tz" -Level OK
}
catch {
Write-Log " Failed to set time zone: $_" -Level ERROR
}
# -----------------------------------------------------------------------
@ -375,9 +321,9 @@ if (Get-Feature $Config "systemRegistry" "edgePolicies") {
# NOTE: No policy key is set intentionally - M365 installation can reinstall
# and run OneDrive normally. Policy DisableFileSyncNGSC would prevent that.
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
Write-Log " Uninstalling OneDrive" -Level INFO
# Remove OneDriveSetup.exe if present
$oneDrivePaths = @(
"$env:SystemRoot\System32\OneDriveSetup.exe"
"$env:SystemRoot\SysWOW64\OneDriveSetup.exe"
@ -385,6 +331,7 @@ if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
foreach ($odPath in $oneDrivePaths) {
if (Test-Path $odPath) {
try {
# Uninstall first
& $odPath /uninstall 2>&1 | Out-Null
Write-Log " OneDrive uninstalled via $odPath" -Level OK
}
@ -400,43 +347,78 @@ if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
Remove-Item $odLnk -Force -ErrorAction SilentlyContinue
Write-Log " Removed OneDrive Start Menu shortcut" -Level OK
}
} else {
Write-Log "oneDriveUninstall feature disabled - skipping" -Level INFO
# -----------------------------------------------------------------------
# Outlook (new) - disable auto-install via UScheduler
# -----------------------------------------------------------------------
Write-Log " Disabling Outlook (new) auto-install" -Level INFO
$uschedulerPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate"
)
foreach ($uPath in $uschedulerPaths) {
if (Test-Path $uPath) {
try {
Remove-Item -Path $uPath -Recurse -Force
Write-Log " Removed UScheduler key: $uPath" -Level OK
}
catch {
Write-Log " Failed to remove UScheduler key: $_" -Level WARN
}
}
}
# -----------------------------------------------------------------------
# Disable GameDVR
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
-Name "AllowGameDVR" -Value 0
# -----------------------------------------------------------------------
# Disable Recall (Windows AI feature)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
-Name "DisableAIDataAnalysis" -Value 1
# -----------------------------------------------------------------------
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
# this policy key ensures the setting survives Windows Updates.
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
-Name "SearchOnTaskbarMode" -Value 0
# -----------------------------------------------------------------------
# Start menu - hide Recommended section (Win11)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" `
-Name "HideRecommendedSection" -Value 1
# -----------------------------------------------------------------------
# Power configuration
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "powercfg") {
Write-Log "Applying power configuration" -Level INFO
$powercfgArgs = @(
$powercfg = @(
@("/change", "standby-timeout-ac", "0"), # never sleep on AC
@("/change", "monitor-timeout-ac", "60"), # screen off after 60 min on AC
@("/change", "standby-timeout-dc", "30"), # sleep after 30 min on battery
@("/change", "monitor-timeout-dc", "15") # screen off after 15 min on battery
)
foreach ($a in $powercfgArgs) {
$result = & powercfg @a 2>&1
foreach ($args in $powercfg) {
$result = & powercfg @args 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " powercfg $($a -join ' ')" -Level OK
Write-Log " powercfg $($args -join ' ')" -Level OK
} else {
Write-Log " powercfg $($a -join ' ') failed: $result" -Level WARN
Write-Log " powercfg $($args -join ' ') failed: $result" -Level WARN
}
}
} else {
Write-Log "powercfg feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Proxy auto-detect disable (WPAD)
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "proxyDisable") {
Write-Log " Disabling WPAD proxy auto-detect" -Level INFO
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" `
-Name "AutoDetect" -Value 0
} else {
Write-Log "proxyDisable feature disabled - skipping" -Level INFO
}
Write-Log "Step 3 complete" -Level OK

View file

@ -36,22 +36,9 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Helper - apply a registry setting to both Default hive and current HKCU
# -----------------------------------------------------------------------
@ -163,10 +150,9 @@ Write-Log "Default hive loaded" -Level OK
try {
# -----------------------------------------------------------------------
# Taskbar tweaks (alignment, buttons, tray, layout XML)
# Taskbar settings (Win10 + Win11)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "taskbarTweaks") {
Write-Log "Applying taskbar tweaks" -Level STEP
Write-Log "Applying taskbar settings" -Level STEP
$tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
@ -191,18 +177,29 @@ try {
# Hide Copilot button
Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0
# Show all tray icons
# Show file extensions in Explorer
Set-ProfileReg -SubKey $tbPath -Name "HideFileExt" -Value 0
# Open Explorer to This PC instead of Quick Access
Set-ProfileReg -SubKey $tbPath -Name "LaunchTo" -Value 1
# -----------------------------------------------------------------------
# System tray - show all icons
# -----------------------------------------------------------------------
# EnableAutoTray = 0 works on Win10; Win11 ignores it but set anyway
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "EnableAutoTray" -Value 0
# Win11 workaround: clear cached tray icon streams so all icons appear on next login
# Windows rebuilds the streams with all icons visible when no cache exists
$trayNotifyKey = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
if (Test-Path $trayNotifyKey) {
Remove-ItemProperty -Path $trayNotifyKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
Remove-ItemProperty -Path $trayNotifyKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
Write-Log " Cleared TrayNotify icon streams (Win11 systray workaround)" -Level OK
}
# Also clear in Default hive so new users start with clean state
$defTrayKey = "Registry::HKU\DefaultProfile\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
if (Test-Path $defTrayKey) {
Remove-ItemProperty -Path $defTrayKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
@ -210,53 +207,82 @@ try {
Write-Log " Cleared TrayNotify icon streams in Default hive" -Level OK
}
# -----------------------------------------------------------------------
# Desktop icons - show This PC
# -----------------------------------------------------------------------
# CLSID {20D04FE0-3AEA-1069-A2D8-08002B30309D} = This PC / Computer
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" `
-Name "{20D04FE0-3AEA-1069-A2D8-08002B30309D}" -Value 0
# -----------------------------------------------------------------------
# Start menu settings
# -----------------------------------------------------------------------
Write-Log "Applying Start menu settings" -Level STEP
# Disable Bing search suggestions in Start menu
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
-Name "DisableSearchBoxSuggestions" -Value 1
# Win11: empty Start menu pins
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" `
-Name "ConfigureStartPins" `
-Value '{"pinnedList":[]}' `
-Type "String"
# Hide "Recently added" apps in Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackProgs" -Value 0
# Hide recently opened files/docs from Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackDocs" -Value 0
# -----------------------------------------------------------------------
# Copilot - disable
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
-Name "TurnOffWindowsCopilot" -Value 1
# -----------------------------------------------------------------------
# GameDVR - disable
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "System\GameConfigStore" `
-Name "GameDVR_Enabled" -Value 0
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
-Name "AppCaptureEnabled" -Value 0
# -----------------------------------------------------------------------
# Num Lock on startup
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Control Panel\Keyboard" `
-Name "InitialKeyboardIndicators" -Value 2 -Type "String"
# -----------------------------------------------------------------------
# Accent color on title bars
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" `
-Name "ColorPrevalence" -Value 1
# -----------------------------------------------------------------------
# Taskbar pinned apps layout (Win10/11)
# ProfileType: default = empty, admin = Explorer+PS+Edge, user = Explorer+Edge
# Note: TaskbarLayoutModification.xml locks the taskbar temporarily.
# UnlockStartLayout scheduled task removes the lock 5 min after first boot
# so users can then customize pins freely.
# Win11 24H2+ may require ProvisionedLayoutModification.xml format instead.
# -----------------------------------------------------------------------
Write-Log "Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO
# Ensure File Explorer shortcut exists in Default profile's Start Menu.
# On a clean Windows 11 install the System Tools folder may be missing
# from C:\Users\Default\AppData\Roaming - without it the XML pin is silently skipped.
$wsh = New-Object -ComObject WScript.Shell
$defRoaming = "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
$explorerLnkDir = "$defRoaming\System Tools"
$explorerLnk = "$explorerLnkDir\File Explorer.lnk"
if (-not (Test-Path $explorerLnk)) {
if (-not (Test-Path $explorerLnkDir)) { New-Item -ItemType Directory -Path $explorerLnkDir -Force | Out-Null }
$sc = $wsh.CreateShortcut($explorerLnk)
$sc.TargetPath = "$env:WINDIR\explorer.exe"
$sc.Save()
Write-Log " Created File Explorer.lnk in Default profile Start Menu" -Level OK
}
# Same for PowerShell (admin profile)
if ($ProfileType -eq "admin") {
$psLnkDir = "$defRoaming\Windows PowerShell"
$psLnk = "$psLnkDir\Windows PowerShell.lnk"
if (-not (Test-Path $psLnk)) {
if (-not (Test-Path $psLnkDir)) { New-Item -ItemType Directory -Path $psLnkDir -Force | Out-Null }
$sc = $wsh.CreateShortcut($psLnk)
$sc.TargetPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
$sc.Save()
Write-Log " Created Windows PowerShell.lnk in Default profile Start Menu" -Level OK
}
}
$taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
if (-not (Test-Path $taskbarLayoutDir)) {
New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
}
# Build pin list based on profile type.
# Paths resolve relative to the new user at first login.
# Missing shortcuts are silently skipped by Windows.
$pinList = switch ($ProfileType) {
"admin" {
@'
@ -271,12 +297,7 @@ try {
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
'@
}
default {
@'
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
'@
} # explicit pins with Replace = no Store, no other defaults
default { "" } # empty = clean slate
}
$taskbarLayoutXml = @"
@ -299,82 +320,6 @@ $pinList
$taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
Write-Log " Taskbar LayoutModification.xml written (profile: $ProfileType)" -Level OK
# NumLock on startup
Set-ProfileReg -SubKey "Control Panel\Keyboard" `
-Name "InitialKeyboardIndicators" -Value 2 -Type "String"
# Accent color on title bars
Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" `
-Name "ColorPrevalence" -Value 1
} else {
Write-Log "taskbarTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Start menu tweaks (pins, Bing, Copilot, GameDVR)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "startMenuTweaks") {
Write-Log "Applying Start menu tweaks" -Level STEP
# Disable Bing search suggestions in Start menu
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
-Name "DisableSearchBoxSuggestions" -Value 1
# Win11: empty Start menu pins
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" `
-Name "ConfigureStartPins" `
-Value '{"pinnedList":[]}' `
-Type "String"
# Hide "Recently added" apps in Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackProgs" -Value 0
# Hide recently opened files/docs from Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackDocs" -Value 0
# Disable Copilot
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
-Name "TurnOffWindowsCopilot" -Value 1
# Disable GameDVR
Set-ProfileReg -SubKey "System\GameConfigStore" `
-Name "GameDVR_Enabled" -Value 0
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
-Name "AppCaptureEnabled" -Value 0
} else {
Write-Log "startMenuTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Explorer tweaks (file extensions, LaunchTo, ShowRecent, FullPath)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "explorerTweaks") {
Write-Log "Applying Explorer tweaks" -Level STEP
$advPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
# Show file extensions in Explorer
Set-ProfileReg -SubKey $advPath -Name "HideFileExt" -Value 0
# Open Explorer to This PC instead of Quick Access
Set-ProfileReg -SubKey $advPath -Name "LaunchTo" -Value 1
# Hide Recent files from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowRecent" -Value 0
# Hide Frequent folders from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowFrequent" -Value 0
# Show full path in Explorer title bar
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" `
-Name "FullPath" -Value 1
} else {
Write-Log "explorerTweaks feature disabled - skipping" -Level INFO
}
}
finally {
# -----------------------------------------------------------------------

View file

@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
@ -94,17 +93,6 @@ function Apply-ThemeSettings {
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
-Name "ColorPrevalence" -Value 1
# -----------------------------------------------------------------------
# Accent color for taskbar
# Windows taskbar reads AccentColorMenu from Explorer\Accent, NOT DWM\AccentColor.
# Without this key the taskbar keeps the Windows default accent (light blue).
# -----------------------------------------------------------------------
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent" `
-Name "AccentColorMenu" -Value $AccentColorABGR -Type "DWord"
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent" `
-Name "StartColorMenu" -Value $AccentColorABGR -Type "DWord"
# -----------------------------------------------------------------------
# Wallpaper - solid color #223B47 (fallback before DesktopInfo runs)
# -----------------------------------------------------------------------
@ -112,12 +100,6 @@ function Apply-ThemeSettings {
Set-Reg -Path "$HiveRoot\Control Panel\Colors" `
-Name "Background" -Value "34 59 71" -Type "String"
# Empty Wallpaper path = solid color from Background key above.
# Without this, new users created from Default hive inherit a broken/missing
# wallpaper path and Windows falls back to black desktop.
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
-Name "Wallpaper" -Value "" -Type "String"
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
-Name "WallpaperStyle" -Value "0" -Type "String"

View file

@ -23,7 +23,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -25,7 +25,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -25,7 +25,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -24,7 +24,6 @@ $ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

View file

@ -1,163 +0,0 @@
<#
.SYNOPSIS
Detects Dell hardware, installs Dell Command | Update, and applies all available updates.
.DESCRIPTION
Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without
error so the same deployment script works on any hardware. On Dell machines, installs
Dell Command | Update (Universal) via winget and immediately runs /applyUpdates with
-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are
staged at this point and finalize automatically during the restart that closes the
deployment. The operator does not need to run a separate update pass after setup.
.ITEMS
detekce-dell-hw-win32-computersystem: Reads Win32_ComputerSystem.Manufacturer. If the string does not contain "Dell", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.
instalace-dell-command-update-via-winget: Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.
spusteni-vsech-aktualizaci-drivery-firmware-bios: Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.
bios-firmware-staging-reboot: BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed.
#>
param(
[object]$Config,
[string]$LogFile
)
$ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
switch ($Level) {
"OK" { Write-Host $line -ForegroundColor Green }
"ERROR" { Write-Host $line -ForegroundColor Red }
"WARN" { Write-Host $line -ForegroundColor Yellow }
"STEP" { Write-Host $line -ForegroundColor Cyan }
default { Write-Host $line }
}
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Detect Dell hardware
# -----------------------------------------------------------------------
Write-Log "Checking hardware manufacturer" -Level INFO
try {
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
$manufacturer = $cs.Manufacturer
$model = $cs.Model
Write-Log " Manufacturer: $manufacturer Model: $model" -Level INFO
}
catch {
Write-Log " Failed to query Win32_ComputerSystem: $_" -Level ERROR
return
}
if ($manufacturer -notmatch "Dell") {
Write-Log "Not a Dell machine ($manufacturer) - step skipped" -Level WARN
return
}
Write-Log "Dell confirmed: $model" -Level OK
# -----------------------------------------------------------------------
# Install Dell Command | Update via winget
# -----------------------------------------------------------------------
Write-Log "Installing Dell Command | Update (Universal)..." -Level STEP
$wingetArgs = @(
"install",
"--id", "Dell.CommandUpdate.Universal",
"--silent",
"--accept-package-agreements",
"--accept-source-agreements"
)
$wingetOutput = & winget @wingetArgs 2>&1
$wingetExit = $LASTEXITCODE
$wingetOutput | ForEach-Object { Write-Log " [winget] $_" -Level INFO }
if ($wingetExit -ne 0 -and $wingetExit -ne 1638) { # 1638 = already installed
Write-Log " winget exit code $wingetExit - checking if DCU is already present" -Level WARN
}
# Locate dcu-cli.exe (path is the same for x64 and Universal edition)
$dcuCandidates = @(
"C:\Program Files\Dell\CommandUpdate\dcu-cli.exe",
"C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe"
)
$dcuCli = $dcuCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $dcuCli) {
Write-Log " dcu-cli.exe not found - cannot run updates" -Level ERROR
return
}
Write-Log " dcu-cli.exe found: $dcuCli" -Level OK
# -----------------------------------------------------------------------
# Run updates - categories controlled by feature flags
# -reboot=disable -> no mid-deployment reboot; BIOS/firmware staged for next restart
# -----------------------------------------------------------------------
$runDrivers = Get-Feature $Config "dellUpdate" "drivers"
$runBios = Get-Feature $Config "dellUpdate" "bios"
if (-not $runDrivers -and -not $runBios) {
Write-Log "Both drivers and bios features disabled - skipping update run" -Level INFO
} else {
# Build update type list from enabled features
$updateTypes = @()
if ($runDrivers) {
$updateTypes += "driver"
$updateTypes += "firmware"
}
if ($runBios) {
$updateTypes += "bios"
}
$updateTypeArg = $updateTypes -join ","
Write-Log "Running Dell Command | Update (updateType=$updateTypeArg, no auto-reboot)..." -Level STEP
Write-Log " This may take several minutes depending on available updates" -Level INFO
$dcuOutput = & $dcuCli /applyUpdates -silent -reboot=disable "-updateType=$updateTypeArg" 2>&1
$exitCode = $LASTEXITCODE
$dcuOutput | ForEach-Object { Write-Log " [DCU] $_" -Level INFO }
Write-Log " DCU exit code: $exitCode" -Level INFO
# Dell Command | Update exit codes:
# 0 = completed, no updates required or updates applied (no reboot needed)
# 1 = updates applied, reboot required to finalize BIOS/firmware
# 5 = no applicable updates found for this system
# others = error or partial failure
switch ($exitCode) {
0 {
Write-Log "Dell Command | Update: complete (no reboot required)" -Level OK
}
1 {
Write-Log "Dell Command | Update: updates staged - BIOS/firmware will finalize on restart" -Level OK
Write-Log "Step 11 complete - signalling reboot needed (exit 9)" -Level OK
exit 9
}
5 {
Write-Log "Dell Command | Update: no applicable updates for this model" -Level OK
}
default {
Write-Log "Dell Command | Update: exit code $exitCode - review DCU log in C:\ProgramData\Dell\UpdateService\Logs" -Level WARN
}
}
}
Write-Log "Step 11 complete" -Level OK

View file

@ -1,85 +0,0 @@
<#
.SYNOPSIS
Installs all available Windows Updates via PSWindowsUpdate module.
.DESCRIPTION
Installs all currently available updates without rebooting.
Exits with code 9 when updates were installed (reboot required for
further rounds) or code 0 when the system is already fully up to date.
The xetup.exe state machine handles the reboot cycle:
- exit 9 -> xetup saves state, sets autologon + X9-Resume task, reboots
- on each subsequent logon X9-Resume launches xetup --resume
- xetup re-runs this step until it exits 0 (no more updates)
- then disables autologon, removes X9-Resume, shows the summary screen
.ITEMS
nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
spustit-kolo-windows-update: One update pass without reboot. Exits 9 when updates were applied (more rounds needed). Exits 0 when system is fully up to date.
#>
param(
[object]$Config,
[string]$LogFile
)
$ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
Write-Output $line
}
Write-Log "=== Step 12 - Windows Update ===" -Level STEP
# -----------------------------------------------------------------------
# 1. NuGet provider + PSWindowsUpdate module
# -----------------------------------------------------------------------
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
$existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
if ($existing) {
Write-Log " PSWindowsUpdate $($existing.Version) already installed" -Level INFO
} else {
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
Write-Log " PSWindowsUpdate installed" -Level OK
}
Import-Module PSWindowsUpdate -Force
} catch {
Write-Log " Module setup failed: $_" -Level ERROR
exit 1
}
# -----------------------------------------------------------------------
# 2. Check and install available updates
# -----------------------------------------------------------------------
Write-Log "Checking for available Windows Updates..." -Level INFO
try {
$available = Get-WindowsUpdate -AcceptAll -IgnoreReboot -ErrorAction Stop
} catch {
Write-Log " Failed to check for updates: $_" -Level ERROR
exit 1
}
if (-not $available -or $available.Count -eq 0) {
Write-Log " System is fully up to date" -Level OK
Write-Log "Step 12 complete" -Level OK
exit 0
}
Write-Log " Found $($available.Count) update(s) - installing..." -Level INFO
try {
$result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
$result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log " $_" -Level INFO }
Write-Log " Update pass complete - reboot required for next round" -Level OK
} catch {
Write-Log " Update install failed: $_" -Level ERROR
exit 1
}
# Signal xetup that a reboot is needed before running this step again
Write-Log "Step 12 - reboot required (exit 9)" -Level OK
exit 9

View file

@ -146,15 +146,5 @@
"povolit-ping-icmp-firewall": "Enables \"File and Printer Sharing (Echo Request)\" firewall rules for ICMPv4 and ICMPv6. ICMP echo is disabled by default on clean Windows. Required for network diagnostics, monitoring tools, and basic connectivity verification.",
"zapnout-network-discovery": "Enables the Network Discovery firewall rule group (FPS-NB_Name-In-UDP, LLMNR, etc.) for Private and Domain profiles via Set-NetFirewallRule. Allows this PC to appear in Network Neighborhood and browse other machines."
}
},
"11-dell-update": {
"synopsis": "Detects Dell hardware, installs Dell Command | Update, and applies all available updates.",
"description": "Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without\nerror so the same deployment script works on any hardware. On Dell machines, installs\nDell Command | Update (Universal) via winget and immediately runs /applyUpdates with\n-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are\nstaged at this point and finalize automatically during the restart that closes the\ndeployment. The operator does not need to run a separate update pass after setup.",
"items": {
"detekce-dell-hw-win32-computersystem": "Reads Win32_ComputerSystem.Manufacturer. If the string does not contain \"Dell\", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.",
"instalace-dell-command-update-via-winget": "Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.",
"spusteni-vsech-aktualizaci-drivery-firmware-bios": "Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.",
"bios-firmware-staging-reboot": "BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed."
}
}
}

View file

@ -1,18 +0,0 @@
# xetup installer - downloads and launches the latest release
# Usage: irm xetup.x9.cz/get.ps1 | iex
$api = 'https://xetup.x9.cz/forgejo-api/repos/x9/xetup/releases?limit=1'
try {
$rel = (Invoke-RestMethod -Uri $api -UseBasicParsing)[0]
$asset = $rel.assets | Where-Object { $_.name -eq 'xetup.exe' } | Select-Object -First 1
if (-not $asset) { Write-Error "xetup.exe not found in latest release"; exit 1 }
$out = "$env:TEMP\xetup.exe"
Write-Host "Downloading xetup $($rel.tag_name)..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $out -UseBasicParsing
Write-Host "Launching..."
Start-Process -FilePath $out -Verb RunAs
} catch {
Write-Error "Failed: $_"
}

View file

@ -34,10 +34,28 @@
align-items: center;
gap: .75rem;
}
.logo-text { font-size: 1.2rem; font-weight: 700; color: #fff; letter-spacing: -.02em; }
.logo-sub { font-size: .8rem; color: var(--muted); margin-left: .2rem; }
header nav { margin-left: auto; display: flex; gap: 1.5rem; }
header nav a { color: var(--muted); text-decoration: none; font-size: .88rem; transition: color .15s; }
.logo-text {
font-size: 1.2rem;
font-weight: 700;
color: #fff;
letter-spacing: -.02em;
}
.logo-sub {
font-size: .8rem;
color: var(--muted);
margin-left: .2rem;
}
header nav {
margin-left: auto;
display: flex;
gap: 1.5rem;
}
header nav a {
color: var(--muted);
text-decoration: none;
font-size: .88rem;
transition: color .15s;
}
header nav a:hover { color: var(--text); }
main {
@ -63,86 +81,92 @@
}
h1 {
font-size: 2.8rem; font-weight: 800; color: #fff;
letter-spacing: -.04em; line-height: 1.1;
margin-bottom: 1rem; max-width: 600px;
font-size: 2.8rem;
font-weight: 800;
color: #fff;
letter-spacing: -.04em;
line-height: 1.1;
margin-bottom: 1rem;
max-width: 600px;
}
h1 span { color: var(--blue); }
.tagline {
font-size: 1.1rem; color: var(--muted); max-width: 480px;
line-height: 1.6; margin-bottom: 2.5rem;
font-size: 1.1rem;
color: var(--muted);
max-width: 480px;
line-height: 1.6;
margin-bottom: 2.5rem;
}
/* ---- ACTIONS ---- */
.actions {
display: flex; gap: .75rem; flex-wrap: wrap;
justify-content: center; margin-bottom: 1.5rem;
display: flex;
gap: .75rem;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 4rem;
}
.btn-primary {
padding: .6rem 1.4rem; background: var(--accent-bright); color: #fff;
border: 1px solid transparent; border-radius: 8px;
font-size: .95rem; font-weight: 600; text-decoration: none; transition: opacity .15s;
padding: .6rem 1.4rem;
background: var(--accent-bright);
color: #fff;
border: 1px solid transparent;
border-radius: 8px;
font-size: .95rem;
font-weight: 600;
text-decoration: none;
transition: opacity .15s;
}
.btn-primary:hover { opacity: .85; }
.btn-secondary {
padding: .6rem 1.4rem; background: transparent; color: var(--text);
border: 1px solid var(--border); border-radius: 8px;
font-size: .95rem; text-decoration: none; transition: background .15s;
padding: .6rem 1.4rem;
background: transparent;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
font-size: .95rem;
text-decoration: none;
transition: background .15s;
}
.btn-secondary:hover { background: var(--card); }
/* ---- INSTALL COMMAND ---- */
.install-box {
margin-bottom: 1.5rem;
display: flex; align-items: center; gap: 0;
background: #0d1117; border: 1px solid var(--border);
border-radius: 10px; overflow: hidden;
font-size: .88rem; max-width: 520px; width: 100%;
}
.install-box code {
flex: 1; padding: .65rem 1rem;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
color: var(--blue); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.install-box button {
padding: .65rem .9rem; background: var(--card);
border: none; border-left: 1px solid var(--border);
color: var(--muted); cursor: pointer; font-size: .8rem;
transition: color .15s, background .15s; white-space: nowrap;
}
.install-box button:hover { background: var(--accent); color: #fff; }
.install-box button.copied { color: var(--green); }
/* ---- DOWNLOAD STRIP ---- */
.download-strip {
margin-bottom: 3.5rem;
display: flex; align-items: center; gap: .6rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: .6rem 1.1rem;
font-size: .85rem;
}
.download-strip a {
color: var(--blue); text-decoration: none; font-weight: 600;
display: flex; align-items: center; gap: .35rem;
}
.download-strip a:hover { text-decoration: underline; }
.download-strip .dl-meta { color: var(--muted); font-size: .78rem; }
.download-strip .dl-sep { color: var(--border); }
/* ---- CARDS ---- */
.cards {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; max-width: 720px; width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
max-width: 720px;
width: 100%;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.2rem;
text-align: left;
}
.card-icon {
font-size: 1.3rem;
margin-bottom: .6rem;
display: block;
}
.card h3 {
font-size: .9rem;
font-weight: 600;
color: #fff;
margin-bottom: .3rem;
}
.card p {
font-size: .82rem;
color: var(--muted);
line-height: 1.4;
}
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1.2rem; text-align: left; }
.card-icon { font-size: 1.3rem; margin-bottom: .6rem; display: block; }
.card h3 { font-size: .9rem; font-weight: 600; color: #fff; margin-bottom: .3rem; }
.card p { font-size: .82rem; color: var(--muted); line-height: 1.4; }
footer {
border-top: 1px solid var(--border); padding: 1.2rem 2rem;
text-align: center; font-size: .8rem; color: var(--muted);
border-top: 1px solid var(--border);
padding: 1.2rem 2rem;
text-align: center;
font-size: .8rem;
color: var(--muted);
}
footer a { color: var(--muted); text-decoration: none; }
footer a:hover { color: var(--text); }
@ -177,27 +201,6 @@
<a href="https://git.xetup.x9.cz/x9/xetup" class="btn-secondary">Git repozitar</a>
</div>
<div class="install-box">
<code id="install-cmd">curl -Lo xetup.exe https://xetup.x9.cz/dl</code>
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
</div>
<p style="color:var(--muted);font-size:.78rem;margin-top:.5rem">
Pokud ve stazeni brani antivirus, pouzijte curl &ndash; staci bezny prikazovy radek, neni potreba administrator.
</p>
<!-- Dynamic download strip filled by JS from Forgejo releases API -->
<div class="download-strip" id="dl-strip" style="display:none">
<a id="dl-link" href="#" download>
&#11015; xetup.exe
</a>
<span class="dl-sep">&middot;</span>
<span class="dl-meta" id="dl-meta">nacitam...</span>
<span class="dl-sep">&middot;</span>
<a href="https://git.xetup.x9.cz/x9/xetup/releases" style="color:var(--muted);font-size:.78rem;text-decoration:none">
vsechny verze
</a>
</div>
<div class="cards">
<div class="card">
<span class="card-icon">&#9881;</span>
@ -217,7 +220,7 @@
<div class="card">
<span class="card-icon">&#128640;</span>
<h3>Go TUI launcher</h3>
<p>xetup.exe &mdash; jednotny binarni spoustec s TUI formularom a live logem.</p>
<p>xetup.exe &mdash; jednotny binarni spoustec. Zatim ve vyvoji.</p>
</div>
</div>
</main>
@ -230,45 +233,5 @@
<a href="/spec/">Specifikace</a>
</footer>
<script>
(function() {
const API = '/forgejo-api';
const REPO = 'x9/xetup';
fetch(API + '/repos/' + REPO + '/releases?limit=1')
.then(r => r.json())
.then(releases => {
if (!Array.isArray(releases) || !releases.length) return;
const rel = releases[0];
const asset = (rel.assets || []).find(a => a.name === 'xetup.exe');
if (!asset) return;
const strip = document.getElementById('dl-strip');
const link = document.getElementById('dl-link');
const meta = document.getElementById('dl-meta');
const sizeMB = (asset.size / 1048576).toFixed(1);
link.href = asset.browser_download_url;
link.textContent = '\u2b15 xetup.exe';
meta.textContent = rel.tag_name + ' \u00b7 ' + sizeMB + ' MB \u00b7 Windows x64';
strip.style.display = '';
})
.catch(() => {});
})();
function copyCmd() {
const cmd = document.getElementById('install-cmd').textContent;
const btn = document.getElementById('copy-btn');
navigator.clipboard.writeText(cmd).then(function() {
btn.textContent = 'Skopirovano!';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Kopirovat';
btn.classList.remove('copied');
}, 2000);
});
}
</script>
</body>
</html>

View file

@ -14,20 +14,5 @@ server {
add_header X-Content-Type-Options nosniff always;
}
# Permanent shortlink to latest xetup.exe never needs updating
location = /dl {
return 302 https://git.xetup.x9.cz/x9/xetup/releases/download/latest/xetup.exe;
}
# Proxy Forgejo API calls so browser doesn't need CORS or direct access to Forgejo
location /forgejo-api/ {
proxy_pass http://xetup-forgejo:3000/api/v1/;
proxy_set_header Host xetup-forgejo;
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;
add_header Cache-Control "no-store" always;
}
error_page 404 /404.html;
}

View file

@ -419,44 +419,6 @@
.comment-submit:disabled { opacity: .4; cursor: default; }
.comment-error { font-size: .75rem; color: var(--red); }
/* ---- NEW REQUEST WIDGET ---- */
.req-list {
margin-bottom: .8rem;
}
.req-item {
padding: .55rem .7rem;
border: 1px solid var(--border);
border-radius: 7px;
margin-bottom: .45rem;
font-size: .83rem;
line-height: 1.5;
background: var(--card2);
}
.req-item-meta { font-size: .72rem; color: var(--muted); margin-bottom: .2rem; }
.req-item-body { color: var(--text); white-space: pre-wrap; word-break: break-word; }
.req-form { display: flex; flex-direction: column; gap: .45rem; }
.req-name {
width: 220px;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); font-size: .8rem; padding: .35rem .5rem; font-family: inherit;
}
.req-text {
width: 100%;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); font-size: .82rem; padding: .4rem .5rem;
resize: vertical; min-height: 70px; font-family: inherit; line-height: 1.4;
}
.req-name:focus, .req-text:focus { outline: none; border-color: var(--accent-bright); }
.req-submit {
align-self: flex-start;
background: var(--accent-bright); color: #fff; border: none;
border-radius: 5px; padding: .32rem .9rem; font-size: .8rem; cursor: pointer;
transition: opacity .15s;
}
.req-submit:hover { opacity: .85; }
.req-submit:disabled { opacity: .4; cursor: default; }
.req-error { font-size: .75rem; color: var(--red); margin-top: .1rem; }
footer {
border-top: 1px solid var(--border);
padding: 1.2rem 2rem;
@ -504,16 +466,15 @@
<a href="#step-06">06 &ndash; Scheduled Tasks</a>
<a href="#step-07">07 &ndash; BackInfo</a>
<a href="#step-08">08 &ndash; Aktivace Windows</a>
<a href="#step-09">09 &ndash; PC identita</a>
<a href="#step-10">10 &ndash; Network discovery</a>
<a href="#step-11">11 &ndash; Dell Command | Update</a>
<a href="#step-12">12 &ndash; Windows Update</a>
<hr class="sidebar-divider">
<h4>Planovane kroky</h4>
<a href="#step-pc">09 &ndash; PC identita + C:\X9</a>
<a href="#step-net">10 &ndash; Network discovery</a>
<a href="#step-taskbar">Taskbar profily</a>
<hr class="sidebar-divider">
<h4>Architektura</h4>
<a href="#arch-xetup">xetup.exe (Go GUI)</a>
<a href="#arch-xetup">xetup.exe (Go TUI)</a>
<a href="#arch-spec">spec.yaml</a>
<hr class="sidebar-divider">
<a href="#new-requests">+ Novy pozadavek</a>
</aside>
<!-- CONTENT -->
@ -521,7 +482,7 @@
<h1>Specifikace &amp; anotace</h1>
<div class="meta">
<span>Verze: 0.4</span>
<span>Verze: 0.3-draft</span>
<span>Datum: 2026-04-16</span>
<span><a href="https://git.xetup.x9.cz/x9/xetup">x9/xetup</a></span>
<span>Status: aktivni vyvoj</span>
@ -639,9 +600,6 @@
<tr class="flag-done"><td>OneDrive uninstall (intentional)</td><td>OneDriveSetup.exe /uninstall &ndash; odstrani pre-installed verzi. M365 si nainstaluje vlastni.</td></tr>
<tr class="flag-done"><td>Powercfg nastaveni (spotreba energie)</td><td>standby-ac 0, monitor-ac 60, standby-dc 30, monitor-dc 15</td></tr>
<tr class="flag-done"><td>Proxy auto-detect zakaz (AutoDetect = 0)</td><td>HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings</td></tr>
<tr class="flag-done"><td>Edge nova karta &ndash; zakaz rychlych odkazu</td><td>NewTabPageQuickLinksEnabled = 0</td></tr>
<tr class="flag-done"><td>Edge nova karta &ndash; zakaz pozadi</td><td>NewTabPageBackgroundEnabled = 0</td></tr>
<tr class="flag-done"><td>Edge nova karta &ndash; zakaz obsahu / feedu</td><td>NewTabPageAllowedBackgroundTypes = 3 (pouze plna barva)</td></tr>
</table>
</div>
<div class="step-footer">
@ -662,7 +620,7 @@
<tr class="flag-done"><td>Taskbar: zarovnat vlevo (TaskbarAl = 0)</td><td>Win11 default je center</td></tr>
<tr class="flag-done"><td>Taskbar: skryt Search, Copilot, Task View, Widgets, Chat</td><td>OK</td></tr>
<tr class="flag-done"><td>Taskbar: zobrazit vsechny ikonky v tray (Scheduled task)</td><td>ShowAllTrayIcons</td></tr>
<tr class="flag-done"><td>Taskbar: explicitni pinlist (TaskbarLayoutModification.xml)</td><td>default/user: Pruzkumnik + Edge; admin: Pruzkumnik + Edge + PowerShell. <code>PinListPlacement="Replace"</code> &ndash; prazdny seznam by dovoloval Windows pridat Store a dalsi vychozi.</td></tr>
<tr class="flag-done"><td>Taskbar: vyprazdnit pinlist (TaskbarLayoutModification.xml)</td><td>OK</td></tr>
<tr class="flag-done"><td>Explorer: zobrazovat pripony souboru (HideFileExt = 0)</td><td>OK</td></tr>
<tr class="flag-done"><td>Explorer: otevrit na This PC (LaunchTo = 1)</td><td>OK</td></tr>
<tr class="flag-done"><td>Start menu: vyprazdnit piny (Win11)</td><td>ConfigureStartPins = {"pinnedList":[]}</td></tr>
@ -702,7 +660,6 @@
<tr class="flag-done"><td>Accent barva na Start a taskbaru: ano</td><td>OK</td></tr>
<tr class="flag-done"><td>Pruhlednost: vypnuta</td><td>OK</td></tr>
<tr class="flag-done"><td>Tapeta: jednobarevna #223B47 (bez obrazku)</td><td>BackInfo prepise tapetu svym BMP</td></tr>
<tr class="flag-done"><td>Wallpaper="" v default hive (oprava cerne tapety)</td><td>Bez tohoto klice novy uzivatel zdedi neplatnou cestu k tapete a dostane cernou plochu. Prazdny retezec = solid barva z <code>Control Panel\Colors\Background</code></td></tr>
</table>
<div class="note">
BackInfo.exe (STEP 07) prepise tapetu BMP se systemovymi informacemi.
@ -787,8 +744,11 @@
</div>
</div>
<!-- ============================================================ -->
<p class="section-label">Nove kroky (planovane)</p>
<!-- STEP 09 -->
<div class="step" id="step-09">
<div class="step" id="step-pc">
<div class="step-header">
<span class="step-num">09</span>
<span class="step-title">PC identita &ndash; Rename + C:\X9</span>
@ -796,13 +756,13 @@
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td>Rename-Computer dle parametru z GUI nebo config.json</td><td><code>deployment.pcName</code> v config.json; preskoci pokud neni nastaveno</td></tr>
<tr class="flag-done"><td>Rename-Computer dle parametru z TUI nebo config.json</td><td><code>deployment.pcName</code> v config.json; preskoci pokud neni nastaveno</td></tr>
<tr class="flag-done"><td>Nastavit popis pocitace (Computer Description)</td><td>LanmanServer\Parameters\SrvComment; default "X9 deployment"</td></tr>
<tr class="flag-done"><td>Vytvorit <code>C:\X9\</code> adresarovou strukturu</td><td>C:\X9\Logs, Scripts, Assets</td></tr>
<tr class="flag-done"><td>Vlastni ikonka pro <code>C:\X9\</code> slozku</td><td>Desktop.ini + X9-ikona.ico z assets\Logo\</td></tr>
</table>
<div class="note">
Rename-Computer vyzaduje restart. Tento krok bezi jako posledni pred finalnim shrnutim &ndash; po nem nasleduje automaticky odpocet a restart v GUI.
Rename-Computer vyzaduje restart. Tento krok bezi jako posledni pred finalnim shrnutim.
</div>
</div>
<div class="step-footer">
@ -812,7 +772,7 @@
</div>
<!-- STEP 10 -->
<div class="step" id="step-10">
<div class="step" id="step-net">
<div class="step-header">
<span class="step-num">10</span>
<span class="step-title">Network discovery + firewall</span>
@ -831,59 +791,7 @@
</div>
</div>
<!-- STEP 11 -->
<div class="step" id="step-11">
<div class="step-header">
<span class="step-num">11</span>
<span class="step-title">Dell Command | Update</span>
<span class="badge badge-ok">OK</span>
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td>Detekce Dell hardware (<code>Win32_ComputerSystem</code>)</td><td>Non-Dell stroj krok preskoci bez chyby &ndash; stejny skript pro vsechny HW</td></tr>
<tr class="flag-done"><td>Instalace Dell Command | Update via winget</td><td><code>Dell.CommandUpdate.Universal</code> &ndash; silent, Win10 + Win11</td></tr>
<tr class="flag-done"><td>Spusteni vsech aktualizaci: drivery, firmware, BIOS</td><td><code>dcu-cli.exe /applyUpdates -silent -reboot=disable</code></td></tr>
<tr class="flag-done"><td>BIOS/firmware se staging &ndash; dokonci se pri restartu</td><td>Restart na konci deploymetu vse dokonci</td></tr>
</table>
<div class="note">
Non-Dell stroje: krok se preskoci automaticky, zadna chyba. Dell Latitude, OptiPlex,
Precision, Vostro, XPS &ndash; vsechny podporovane DCU Universal.<br><br>
<strong>Casova narocnost:</strong> 5&ndash;20 minut podle poctu dostupnych aktualizaci a rychlosti sitoveho pripojeni.
</div>
</div>
<div class="step-footer">
<span class="step-status">Script: <code>11-dell-update.ps1</code></span>
<div class="comment-widget" data-issue="16"></div>
</div>
</div>
<!-- STEP 12 -->
<div class="step" id="step-12">
<div class="step-header">
<span class="step-num">12</span>
<span class="step-title">Windows Update</span>
<span class="badge badge-ok">OK</span>
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td>Instalace NuGet providera + PSWindowsUpdate modulu ze PSGallery</td><td>Install-PackageProvider + Install-Module PSWindowsUpdate -Force</td></tr>
<tr class="flag-done"><td>Prvni pruchod aktualizaci (bez rebootu)</td><td>Install-WindowsUpdate -AcceptAll -IgnoreReboot</td></tr>
<tr class="flag-done"><td>X9-WindowsUpdate scheduled task &ndash; pokracovani po restartu</td><td>Bezi pri kazdem logonu jako SYSTEM; po pruchodu bez novych update se task sam smaze</td></tr>
<tr class="flag-done"><td>Automaticky restart po skonceni deploymetu (GUI odpocet)</td><td>xetup.exe zobrazi 60s odpocet + tlacitka "Restartovat ted" / "Zrusit restart"</td></tr>
</table>
<div class="note">
Windows Update typicky vyzaduje vice restartovacich kolu. Prvni pruchod probehne behem deploymetu,
dalsi kola zaridti scheduled task, ktery se po dokonceni sam odregistruje.<br><br>
<strong>Pozor:</strong> Prvni restart spusti jak Windows Update task, tak Dell firmware staging (krok 11).
</div>
</div>
<div class="step-footer">
<span class="step-status">Script: <code>12-windows-update.ps1</code></span>
<div class="comment-widget" data-issue="17"></div>
</div>
</div>
<!-- TASKBAR PROFILY - detail implementace kroku 04 -->
<!-- TASKBAR -->
<div class="step" id="step-taskbar">
<div class="step-header">
<span class="step-num">04+</span>
@ -893,13 +801,14 @@
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td><code>-ProfileType</code> parametr: default / admin / user</td><td>Parametr GUI nebo config.json; predano do skriptu 04</td></tr>
<tr class="flag-done"><td>XML layout pro "default" a "user": Pruzkumnik + Edge</td><td><code>PinListPlacement="Replace"</code> &ndash; explicitni seznam zamezuje pridani Store a dalsich vychozich appek</td></tr>
<tr class="flag-done"><td>XML layout pro "admin": Pruzkumnik + Edge + PowerShell</td><td>TaskbarLayoutModification.xml; PowerShell.lnk z System32</td></tr>
<tr class="flag-done"><td><code>-ProfileType</code> parametr: admin vs user varianta</td><td>Deploy-Windows.ps1 -ProfileType [default|admin|user]; predano do 04</td></tr>
<tr class="flag-done"><td>XML layout pro "admin": Explorer, PS, Edge</td><td>TaskbarLayoutModification.xml; File Explorer.lnk + PowerShell.lnk + Edge.lnk</td></tr>
<tr class="flag-done"><td>XML layout pro "user": Explorer, Edge</td><td>Konzervativni sada &ndash; Outlook/Teams pridany az po instalaci M365</td></tr>
<tr class="flag-open"><td>Win11 24H2 kompatibilita layoutu</td><td>24H2 vyzaduje ProvisionedLayoutModification.xml &ndash; nutno otestovat na realne instalaci</td></tr>
</table>
<div class="note">
Layout se pri aplikaci zamkne. UnlockStartLayout task (krok 06) ho odemkne 5 min po startu, aby uzivatel mohl dale upravovat.
Aplikace pinnutych appek: <code>Deploy-Windows.ps1 -ProfileType admin</code> nebo <code>-ProfileType user</code>.<br>
Layout se zablokuje, UnlockStartLayout task (krok 06) ho odemkne 5 min po startu.
</div>
</div>
<div class="step-footer">
@ -909,37 +818,35 @@
</div>
<!-- ============================================================ -->
<p class="section-label">Architektura</p>
<p class="section-label">Architektura (budoucnost)</p>
<!-- ARCH XETUP -->
<div class="step" id="arch-xetup">
<div class="step-header">
<span class="step-num">Arc</span>
<span class="step-title">xetup.exe &ndash; Go GUI (Walk / Win32)</span>
<span class="badge badge-ok">OK</span>
<span class="step-title">xetup.exe &ndash; Go TUI launcher</span>
<span class="badge badge-future">Future</span>
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td>Single binary (go:embed scripty + assets)</td><td><code>embed.go</code> + <code>cmd/xetup/main.go</code>; ~5 MB .exe, zadne externi zavislosti</td></tr>
<tr class="flag-done"><td>Walk GUI &ndash; Win32 nativni ovladky, bez OpenGL</td><td>Funguje na VMware ESXi / SVGA II (Fyne/OpenGL nefungovalo). <code>internal/gui/gui.go</code></td></tr>
<tr class="flag-done"><td>Formular: PC jmeno, popis, product key, profil</td><td>Faze 1 &ndash; config form pred spustenim</td></tr>
<tr class="flag-done"><td>Checklist kroku (on/off per-feature) + nacist / ulozit config.json</td><td>Tlacitka "Nacist config..." a "Ulozit config..." pro per-klient presety</td></tr>
<tr class="flag-done"><td>Live log + prubehovy strip kroku (0&ndash;12)</td><td>Barevne indikatory: · cekajici, &#x25ba; bezici (modra), &#x2713; OK (zelena), &#x2717; chyba (cervena)</td></tr>
<tr class="flag-done"><td>Summary s odpoctem a automatickym restartem</td><td>Faze 3 &ndash; 60s odpocet, tlacitka "Restartovat ted" / "Zrusit restart"</td></tr>
<tr class="flag-done"><td>UAC requireAdministrator (app.manifest + rsrc)</td><td>Windows vyzada elevaci pri spusteni; ComCtl32 v6 + DPI awareness</td></tr>
<tr class="flag-done"><td>PowerShell okno skryte na pozadi</td><td>SysProcAttr.HideWindow = true; PS chyby filtrovane (At line:, CategoryInfo atd.)</td></tr>
<tr class="flag-todo"><td>Self-update: stahnout novou verzi z xetup.x9.cz/dl</td><td>Overit hash pred spustenim</td></tr>
<tr class="flag-future"><td>config.json: per-klient preset na USB</td><td>Jmeno PC prefix, SW seznam, klic &ndash; lezi vedle .exe</td></tr>
<tr class="flag-todo"><td>Single binary (go:embed scripty + assets)</td><td>Offline provoz, jedna stazitelna .exe</td></tr>
<tr class="flag-todo"><td>TUI form (huh/bubbletea): PC name, popis, product key</td><td>Interaktivni zadani dat technikem</td></tr>
<tr class="flag-todo"><td>Checklist kroku (on/off per-script) + ulozit do config.json</td><td>Opakovatelne nasazeni u stejneho klienta</td></tr>
<tr class="flag-todo"><td>Live log output behem spousteni PS scriptu</td><td>Stdout z powershell.exe v realnem case</td></tr>
<tr class="flag-todo"><td>Finalni summary OK/ERROR</td><td>Na konci nasazeni</td></tr>
<tr class="flag-todo"><td>Self-update: stahnout novou verzi z xetup.x9.cz</td><td>Overit hash pred spustenim</td></tr>
<tr class="flag-future"><td>config.json: per-klient preset (prefix jmena PC, SW, klic)</td><td>Lezi vedle .exe na USB klienta</td></tr>
<tr class="flag-future"><td>OpenVPN soubor + doménovy join + domén. uzivatel pro profil</td><td>Rozsireni TUI formulare v budoucnu</td></tr>
</table>
<div class="note">
<strong>Struktura:</strong> <code>cmd/xetup/</code>, <code>internal/config/</code>, <code>internal/gui/</code>, <code>internal/runner/</code><br><br>
<strong>Go zavislosti:</strong> github.com/lxn/walk (Win32 GUI), golang.org/x/sys<br><br>
<strong>Build:</strong> <code>GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" ./cmd/xetup/</code><br>
CI (Forgejo Actions) sestavi a publikuje automaticky pri kazdem push na main.
<strong>Struktura:</strong> <code>cmd/xetup/</code>, <code>internal/config/</code>,
<code>internal/spec/</code>, <code>internal/tui/</code>, <code>internal/runner/</code><br><br>
<strong>Go zavislosti:</strong>
bubbletea (TUI framework), huh (forms), lipgloss (styling)
</div>
</div>
<div class="step-footer">
<span class="step-status">Status: v provozu &ndash; <a href="https://xetup.x9.cz/dl" style="color:var(--blue)">xetup.x9.cz/dl</a></span>
<span class="step-status">Status: navrh, zatim zadny kod</span>
<div class="comment-widget" data-issue="11"></div>
</div>
</div>
@ -978,34 +885,6 @@
</div>
</div>
<!-- ============================================================ -->
<p class="section-label">Nove nastaveni &ndash; pozadavky</p>
<!-- NEW REQUESTS -->
<div class="step" id="new-requests">
<div class="step-header">
<span class="step-num" style="font-size:1.1rem">+</span>
<span class="step-title">Novy pozadavek na automatizaci</span>
<span class="badge badge-new">Pozadavky</span>
</div>
<div class="step-body">
<p style="color:var(--muted);font-size:.84rem;line-height:1.5;margin-bottom:1rem">
Chcete automatizovat neco, co skript zatim neresi?
Napiste pozadavek sem &ndash; ulozi se do repozitare.
Technicky tym ho projde a zaradi do planu.
</p>
<div class="req-list" id="req-list">
<div style="font-size:.8rem;color:var(--muted);font-style:italic">Nacitam pozadavky...</div>
</div>
<div class="req-form" id="req-form">
<input class="req-name" id="req-name" placeholder="Vase jmeno (volitelne)" maxlength="60">
<textarea class="req-text" id="req-text" placeholder="Co by mel automat delat? Popiste konkretne, idealne i proc." rows="3"></textarea>
<button class="req-submit" id="req-submit">Odeslat pozadavek</button>
<span class="req-error" id="req-error"></span>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /layout -->
@ -1015,8 +894,6 @@
<a href="https://git.xetup.x9.cz/x9/xetup">Forgejo</a>
&nbsp;&middot;&nbsp;
<a href="/">xetup.x9.cz</a>
&nbsp;&middot;&nbsp;
web: <span id="deploy-sha" style="font-family:monospace">...</span>
</footer>
<script>
@ -1157,11 +1034,9 @@
'step-05': '05-personalization',
'step-06': '06-scheduled-tasks',
'step-07': '07-backinfo',
'step-pc': '09-pc-identity',
'step-net': '10-network',
'step-08': '08-activation',
'step-09': '09-pc-identity',
'step-10': '10-network',
'step-11': '11-dell-update',
'step-12': '12-windows-update',
};
function getItemDesc(stepId, slug) {
@ -1314,83 +1189,7 @@
});
}
// ---- NEW-REQUEST WIDGET (issue #15) ----
(function() {
const ISSUE = 15;
const listEl = document.getElementById('req-list');
const nameEl = document.getElementById('req-name');
const textEl = document.getElementById('req-text');
const submitEl = document.getElementById('req-submit');
const errorEl = document.getElementById('req-error');
if (!listEl) return;
function renderRequests(data) {
listEl.innerHTML = '';
if (!data.length) {
listEl.innerHTML = '<div style="font-size:.8rem;color:var(--muted);font-style:italic;margin-bottom:.5rem">Zatim zadne pozadavky. Bud prvni.</div>';
return;
}
data.forEach(c => {
const m = c.body.match(/^\*\*(.+?)\*\*\n([\s\S]*)$/);
const name = m ? m[1] : (c.user.login === 'xetup-bot' ? 'anon' : c.user.login);
const text = m ? m[2] : c.body;
const el = document.createElement('div');
el.className = 'req-item';
el.innerHTML =
'<div class="req-item-meta">' + name.replace(/</g,'&lt;') + ' &middot; ' + timeAgo(c.created_at) + '</div>' +
'<div class="req-item-body">' + text.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</div>';
listEl.appendChild(el);
});
}
fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', { headers: HEADS })
.then(r => r.json())
.then(d => renderRequests(Array.isArray(d) ? d : []))
.catch(() => { listEl.innerHTML = '<div style="font-size:.8rem;color:var(--red)">Chyba nacitani.</div>'; });
submitEl.addEventListener('click', () => {
const text = textEl.value.trim();
if (!text) { errorEl.textContent = 'Pozadavek nesmi byt prazdny.'; return; }
errorEl.textContent = '';
submitEl.disabled = true;
submitEl.textContent = 'Odesilam...';
const name = nameEl.value.trim() || 'anon';
const body = '**' + name + '**\n' + text;
fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', {
method: 'POST', headers: HEADS, body: JSON.stringify({ body: body })
})
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(c => {
textEl.value = ''; nameEl.value = '';
submitEl.disabled = false;
submitEl.textContent = 'Odeslat pozadavek';
// re-fetch to show updated list
return fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', { headers: HEADS });
})
.then(r => r.json())
.then(d => renderRequests(Array.isArray(d) ? d : []))
.catch(() => {
errorEl.textContent = 'Chyba odeslani. Zkus znovu.';
submitEl.disabled = false;
submitEl.textContent = 'Odeslat pozadavek';
});
});
})();
enhanceRows();
// Show deployed commit in footer
fetch('/data/deploy.json')
.then(r => r.json())
.then(d => {
const el = document.getElementById('deploy-sha');
if (el && d.sha) {
const ts = d.ts ? ' (' + new Date(d.ts).toLocaleString('cs-CZ', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) + ')' : '';
el.textContent = d.sha + ts;
el.title = d.ts || '';
}
})
.catch(() => {});
})();
</script>
</body>

BIN
xetup.exe

Binary file not shown.