diff --git a/README.md b/README.md index 641e367..4e9f121 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,12 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Deutsch** — učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause) a 4 ročníky, 24 lekcí od A1 k B1: poslech, čtení, psaní, doplňování a kvízy -- **Správa počítačových sítí** — 1. ročník, 27 lekcí (VLSM + teorie a kvízy); - ročníky 2–4 jsou zamčené -- **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené +- **Správa počítačových sítí** — 4 ročníky: 1. ročník má 27 lekcí + (adresy, modely, přepínače a bezdrát), ročníky 2–4 po 8 lekcích + (VLAN, směrování, firewall, návrh a maturita) +- **Technické vybavení** — 4 ročníky: 1. ročník má 29 lekcí + (architektura, data, skříň, deska, karty, disky a vstup), + ročníky 2–4 po 8 lekcích (monitory, sestavení, RAID, servery, údržba) - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov středních škol), mluvnice (20 cvičení) a maturitní četba (82 knih: zápisky, kvíz, sestavení děje) @@ -268,9 +271,12 @@ Intel Mac, another architecture, or hacking on the code → - **Deutsch** — the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause) plus 4 years, 24 lessons from A1 to B1: listening, reading, writing, gap-fill and quizzes -- **Computer networks** — year 1, 27 lessons (VLSM + theory/quizzes); - years 2–4 are locked -- **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked +- **Computer networks** — 4 years: year 1 has 27 lessons + (addresses, models, switches and wireless), years 2–4 have 8 lessons each + (VLANs, routing, firewalls, design and the exam) +- **Computer hardware** — 4 years: year 1 has 29 lessons + (architecture, data, case, board, cards, drives and input), + years 2–4 have 8 lessons each (displays, assembly, RAID, servers, maintenance) - **Czech language** — literature (4 years, 32 lessons from the secondary-school syllabus), grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering) diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index 480484b..bb41144 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -156,6 +156,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markCourse(course: String, id: Int) { + progress.markCourse(course, id) + refresh() + } + fun markLit(id: Int) { progress.markLit(id) refresh() diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index 6cbe0f3..94d8215 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -51,7 +51,9 @@ import org.maturita.maturita.ui.On4QuizScreen import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnYearsScreen import org.maturita.maturita.ui.EnYearsScreen +import org.maturita.maturita.ui.ChoiceLessonScreen import org.maturita.maturita.ui.DeHomeScreen +import org.maturita.maturita.ui.ItYearScreen import org.maturita.maturita.ui.EngExScreen import org.maturita.maturita.ui.PlotScreen import org.maturita.maturita.ui.RoadmapScreen @@ -244,6 +246,30 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.NetEx -> NetQuizScreen(vm, r.id) + Route.Net2Map -> ItYearScreen(vm, "net2", "net_year2", "net2_sub") { Route.Net2Lesson(it) } + is Route.Net2Lesson -> { + val l = vm.content.itLesson("net2", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Net2Ex(r.id)) + } + } + is Route.Net2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net2", r.id)) { vm.markCourse("net2", r.id) } + Route.Net3Map -> ItYearScreen(vm, "net3", "net_year3", "net3_sub") { Route.Net3Lesson(it) } + is Route.Net3Lesson -> { + val l = vm.content.itLesson("net3", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Net3Ex(r.id)) + } + } + is Route.Net3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net3", r.id)) { vm.markCourse("net3", r.id) } + Route.Net4Map -> ItYearScreen(vm, "net4", "net_year4", "net4_sub") { Route.Net4Lesson(it) } + is Route.Net4Lesson -> { + val l = vm.content.itLesson("net4", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Net4Ex(r.id)) + } + } + is Route.Net4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net4", r.id)) { vm.markCourse("net4", r.id) } Route.HwYears -> HwYearsScreen(vm) Route.HwMap -> LessonListScreen( vm, vm.tr("hw_year1"), vm.tr("hw_sub"), @@ -259,6 +285,30 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.HwEx -> HwQuizScreen(vm, r.id) + Route.Hw2Map -> ItYearScreen(vm, "hw2", "hw_year2", "hw2_sub") { Route.Hw2Lesson(it) } + is Route.Hw2Lesson -> { + val l = vm.content.itLesson("hw2", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Hw2Ex(r.id)) + } + } + is Route.Hw2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw2", r.id)) { vm.markCourse("hw2", r.id) } + Route.Hw3Map -> ItYearScreen(vm, "hw3", "hw_year3", "hw3_sub") { Route.Hw3Lesson(it) } + is Route.Hw3Lesson -> { + val l = vm.content.itLesson("hw3", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Hw3Ex(r.id)) + } + } + is Route.Hw3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw3", r.id)) { vm.markCourse("hw3", r.id) } + Route.Hw4Map -> ItYearScreen(vm, "hw4", "hw_year4", "hw4_sub") { Route.Hw4Lesson(it) } + is Route.Hw4Lesson -> { + val l = vm.content.itLesson("hw4", r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Hw4Ex(r.id)) + } + } + is Route.Hw4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw4", r.id)) { vm.markCourse("hw4", r.id) } Route.EnYears -> EnYearsScreen(vm) is Route.EnMap -> LessonListScreen( vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"), diff --git a/android/app/src/main/java/org/maturita/maturita/Route.kt b/android/app/src/main/java/org/maturita/maturita/Route.kt index 3c7665b..c90685e 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -17,10 +17,28 @@ sealed class Route(val page: String) { data object NetMap : Route("netmap") data class NetLesson(val id: Int) : Route("netunit$id") data class NetEx(val id: Int) : Route("netex$id") + data object Net2Map : Route("net2map") + data class Net2Lesson(val id: Int) : Route("net2unit$id") + data class Net2Ex(val id: Int) : Route("net2ex$id") + data object Net3Map : Route("net3map") + data class Net3Lesson(val id: Int) : Route("net3unit$id") + data class Net3Ex(val id: Int) : Route("net3ex$id") + data object Net4Map : Route("net4map") + data class Net4Lesson(val id: Int) : Route("net4unit$id") + data class Net4Ex(val id: Int) : Route("net4ex$id") data object HwYears : Route("hwyears") data object HwMap : Route("hwmap") data class HwLesson(val id: Int) : Route("hwunit$id") data class HwEx(val id: Int) : Route("hwex$id") + data object Hw2Map : Route("hw2map") + data class Hw2Lesson(val id: Int) : Route("hw2unit$id") + data class Hw2Ex(val id: Int) : Route("hw2ex$id") + data object Hw3Map : Route("hw3map") + data class Hw3Lesson(val id: Int) : Route("hw3unit$id") + data class Hw3Ex(val id: Int) : Route("hw3ex$id") + data object Hw4Map : Route("hw4map") + data class Hw4Lesson(val id: Int) : Route("hw4unit$id") + data class Hw4Ex(val id: Int) : Route("hw4ex$id") data object OnYears : Route("onyears") data object OnMap : Route("onmap") data class OnLesson(val id: Int) : Route("onunit$id") @@ -102,6 +120,8 @@ sealed class Route(val page: String) { val isExercise: Boolean get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is EnLesson || this is EnEx || this is DeLesson || this is DeEx || this is MluvEx || this is BookQuiz || this is BookPlot || + this is Net2Lesson || this is Net2Ex || this is Net3Lesson || this is Net3Ex || this is Net4Lesson || this is Net4Ex || + this is Hw2Lesson || this is Hw2Ex || this is Hw3Lesson || this is Hw3Ex || this is Hw4Lesson || this is Hw4Ex || this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson || this is On4Lesson || this is LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex || this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || @@ -129,10 +149,28 @@ sealed class Route(val page: String) { NetMap -> listOf(Subjects, NetYears, NetMap) is NetLesson -> listOf(Subjects, NetYears, NetMap, this) is NetEx -> listOf(Subjects, NetYears, NetMap, NetLesson(id), this) + Net2Map -> listOf(Subjects, NetYears, Net2Map) + is Net2Lesson -> listOf(Subjects, NetYears, Net2Map, this) + is Net2Ex -> listOf(Subjects, NetYears, Net2Map, Net2Lesson(id), this) + Net3Map -> listOf(Subjects, NetYears, Net3Map) + is Net3Lesson -> listOf(Subjects, NetYears, Net3Map, this) + is Net3Ex -> listOf(Subjects, NetYears, Net3Map, Net3Lesson(id), this) + Net4Map -> listOf(Subjects, NetYears, Net4Map) + is Net4Lesson -> listOf(Subjects, NetYears, Net4Map, this) + is Net4Ex -> listOf(Subjects, NetYears, Net4Map, Net4Lesson(id), this) HwYears -> listOf(Subjects, HwYears) HwMap -> listOf(Subjects, HwYears, HwMap) is HwLesson -> listOf(Subjects, HwYears, HwMap, this) is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this) + Hw2Map -> listOf(Subjects, HwYears, Hw2Map) + is Hw2Lesson -> listOf(Subjects, HwYears, Hw2Map, this) + is Hw2Ex -> listOf(Subjects, HwYears, Hw2Map, Hw2Lesson(id), this) + Hw3Map -> listOf(Subjects, HwYears, Hw3Map) + is Hw3Lesson -> listOf(Subjects, HwYears, Hw3Map, this) + is Hw3Ex -> listOf(Subjects, HwYears, Hw3Map, Hw3Lesson(id), this) + Hw4Map -> listOf(Subjects, HwYears, Hw4Map) + is Hw4Lesson -> listOf(Subjects, HwYears, Hw4Map, this) + is Hw4Ex -> listOf(Subjects, HwYears, Hw4Map, Hw4Lesson(id), this) OnYears -> listOf(Subjects, OnYears) OnMap -> listOf(Subjects, OnYears, OnMap) is OnLesson -> listOf(Subjects, OnYears, OnMap, this) @@ -218,6 +256,12 @@ fun routeFromPage(page: String): Route? = when (page) { "dehome" -> Route.DeHome "netyears" -> Route.NetYears "netmap" -> Route.NetMap + "net2map" -> Route.Net2Map + "net3map" -> Route.Net3Map + "net4map" -> Route.Net4Map + "hw2map" -> Route.Hw2Map + "hw3map" -> Route.Hw3Map + "hw4map" -> Route.Hw4Map "hwyears" -> Route.HwYears "hwmap" -> Route.HwMap "onyears" -> Route.OnYears @@ -375,6 +419,42 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""de(\d)ex(\d+)""").matchEntire(page)?.let { return Route.DeEx(it.groupValues[1].toInt(), it.groupValues[2].toInt()) } + Regex("""net([234])unit(\d+)""").matchEntire(page)?.let { + val year = it.groupValues[1].toInt() + val id = it.groupValues[2].toInt() + return when (year) { + 3 -> Route.Net3Lesson(id) + 4 -> Route.Net4Lesson(id) + else -> Route.Net2Lesson(id) + } + } + Regex("""net([234])ex(\d+)""").matchEntire(page)?.let { + val year = it.groupValues[1].toInt() + val id = it.groupValues[2].toInt() + return when (year) { + 3 -> Route.Net3Ex(id) + 4 -> Route.Net4Ex(id) + else -> Route.Net2Ex(id) + } + } + Regex("""hw([234])unit(\d+)""").matchEntire(page)?.let { + val year = it.groupValues[1].toInt() + val id = it.groupValues[2].toInt() + return when (year) { + 3 -> Route.Hw3Lesson(id) + 4 -> Route.Hw4Lesson(id) + else -> Route.Hw2Lesson(id) + } + } + Regex("""hw([234])ex(\d+)""").matchEntire(page)?.let { + val year = it.groupValues[1].toInt() + val id = it.groupValues[2].toInt() + return when (year) { + 3 -> Route.Hw3Ex(id) + 4 -> Route.Hw4Ex(id) + else -> Route.Hw2Ex(id) + } + } Regex("""en(\d)map""").matchEntire(page)?.let { return Route.EnMap(it.groupValues[1].toInt()) } diff --git a/android/app/src/main/java/org/maturita/maturita/data/Content.kt b/android/app/src/main/java/org/maturita/maturita/data/Content.kt index 57dec5d..a0af855 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Content.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Content.kt @@ -29,6 +29,12 @@ class Content(root: JSONObject) { } } val hw = raw.arr("hw") + val net2 = raw.arr("net2") + val net3 = raw.arr("net3") + val net4 = raw.arr("net4") + val hw2 = raw.arr("hw2") + val hw3 = raw.arr("hw3") + val hw4 = raw.arr("hw4") val on = raw.arr("on") val on2 = raw.arr("on2") val on3 = raw.arr("on3") @@ -63,6 +69,16 @@ class Content(root: JSONObject) { fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id } fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id } fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id } + fun itLessons(course: String) = when (course) { + "net2" -> net2 + "net3" -> net3 + "net4" -> net4 + "hw2" -> hw2 + "hw3" -> hw3 + "hw4" -> hw4 + else -> emptyList() + } + fun itLesson(course: String, id: Int) = itLessons(course).firstOrNull { it.int("id") == id } fun onLesson(id: Int) = on.firstOrNull { it.int("id") == id } fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id } fun on3Lesson(id: Int) = on3.firstOrNull { it.int("id") == id } diff --git a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt index c716c61..a1f96b5 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt @@ -48,6 +48,8 @@ class ProgressStore(context: Context) { fun markEn(year: Int, id: Int) { prefs.edit().putBoolean("en.$year.$id", true).apply() } fun deDone(year: Int, id: Int) = prefs.getBoolean("de.$year.$id", false) fun markDe(year: Int, id: Int) { prefs.edit().putBoolean("de.$year.$id", true).apply() } + fun courseDone(course: String, id: Int) = prefs.getBoolean("$course.$id", false) + fun markCourse(course: String, id: Int) { prefs.edit().putBoolean("$course.$id", true).apply() } fun litDone(id: Int) = prefs.getBoolean("lit.$id", false) fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() } @@ -119,6 +121,16 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + listOf("net2", "net3", "net4", "hw2", "hw3", "hw4").forEach { course -> + content.itLessons(course).forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.courseDone(course, l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + } content.on.forEach { l -> sum.totalEx += 1 sum.openUnits += 1 diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt index e6273cb..e876e30 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt @@ -1077,6 +1077,18 @@ private fun MathBoard( } } +@Composable +fun ChoiceLessonScreen(vm: AppViewModel, lesson: J?, onDone: () -> Unit) { + if (lesson == null) return + val spec = J(org.json.JSONObject().apply { + put("type", "choice") + put("title", vm.tr(lesson.str("exTitleKey"))) + put("sub", lesson.str("quizHeadKey")) + put("questions", lesson.o.optJSONArray("quiz")) + }) + DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey")), onDone) +} + @Composable fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) { val lesson = when (year) { diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index 3c94f9d..d019ca1 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -306,12 +306,16 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { } } "netyears" -> { - sum.totalEx = vm.content.netLessons.size - sum.doneEx = vm.content.netLessons.count { vm.progress.netDone(it.int("id")) } + val extra = listOf("net2", "net3", "net4") + sum.totalEx = vm.content.netLessons.size + extra.sumOf { vm.content.itLessons(it).size } + sum.doneEx = vm.content.netLessons.count { vm.progress.netDone(it.int("id")) } + + extra.sumOf { course -> vm.content.itLessons(course).count { vm.progress.courseDone(course, it.int("id")) } } } "hwyears", "hwmap" -> { - sum.totalEx = vm.content.hw.size - sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } + val extra = listOf("hw2", "hw3", "hw4") + sum.totalEx = vm.content.hw.size + extra.sumOf { vm.content.itLessons(it).size } + sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } + + extra.sumOf { course -> vm.content.itLessons(course).count { vm.progress.courseDone(course, it.int("id")) } } } "enyears", "en1map", "en2map", "en3map", "en4map" -> { sum.totalEx = (1..4).sumOf { vm.content.enYear(it).size } @@ -530,10 +534,19 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit${l.int("id")}", extra = "site") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex${l.int("id")}") } + listOf("net2", "net3", "net4").forEach { course -> + add(vm.tr("net_year${course.drop(3)}"), vm.tr("search_lesson"), "${course}map", extra = "rocnik site") + vm.content.itLessons(course).forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "${course}unit${l.int("id")}", extra = "site") + } + } add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra = "rocnik") - add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") - add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") - add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") + listOf("hw2", "hw3", "hw4").forEach { course -> + add(vm.tr("hw_year${course.drop(2)}"), vm.tr("search_lesson"), "${course}map", extra = "rocnik hardware") + vm.content.itLessons(course).forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "${course}unit${l.int("id")}", extra = "hardware") + } + } vm.content.hw.forEach { l -> add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}") @@ -718,15 +731,26 @@ fun UnitMapScreen(vm: AppViewModel, unitId: Int) { } } +@Composable +fun ItYearScreen(vm: AppViewModel, course: String, titleKey: String, subKey: String, lesson: (Int) -> Route) { + LessonListScreen( + vm, vm.tr(titleKey), vm.tr(subKey), + vm.content.itLessons(course).map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.courseDone(course, id), lesson(id)) + }, + ) +} + @Composable fun NetYearsScreen(vm: AppViewModel) { DetailScaffold(vm, vm.tr("net_years_title"), vm.tr("net_years_sub")) { inner -> LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { item { NavRow(title = vm.tr("net_year1"), subtitle = vm.tr("net_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.NetMap) } - } - items(listOf("net_year2", "net_year3", "net_year4")) { key -> - NavRow(title = vm.tr(key), subtitle = vm.tr("net_year_locked_sub"), locked = true, onClick = null) + NavRow(title = vm.tr("net_year2"), subtitle = vm.tr("net2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net2Map) } + NavRow(title = vm.tr("net_year3"), subtitle = vm.tr("net3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net3Map) } + NavRow(title = vm.tr("net_year4"), subtitle = vm.tr("net4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net4Map) } } } } @@ -738,9 +762,9 @@ fun HwYearsScreen(vm: AppViewModel) { LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { item { NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.HwMap) } - } - items(listOf("hw_year2", "hw_year3", "hw_year4")) { key -> - NavRow(title = vm.tr(key), subtitle = vm.tr("hw_year_locked_sub"), locked = true, onClick = null) + NavRow(title = vm.tr("hw_year2"), subtitle = vm.tr("hw2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw2Map) } + NavRow(title = vm.tr("hw_year3"), subtitle = vm.tr("hw3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw3Map) } + NavRow(title = vm.tr("hw_year4"), subtitle = vm.tr("hw4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw4Map) } } } } diff --git a/data/changelog.txt b/data/changelog.txt index 05faf8a..3cd6823 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -6,6 +6,12 @@ # cs: bullet # en: bullet +--- 2026-10-07 +cs: Sítě a technické vybavení: doplněné 2. až 4. ročníky, v každém 8 lekcí +cs: Výklad a kvíz od VLAN a sestavení počítače po maturitu +en: Networks and hardware: years 2 to 4 are filled in, eight lessons each +en: Slides and a quiz, from VLANs and PC assembly through to the exam + --- 2026-10-07 cs: Němčina: čtyři ročníky, 24 lekcí s poslechem, čtením a psaním cs: Učebnice Neue Freunde zůstává, k ní přibyl kurz od A1 k B1 diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index e6c0e0b..ce12ef6 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -176,6 +176,11 @@ final class AppModel: ObservableObject { refresh() } + func markCourse(_ course: String, _ id: Int) { + progress.markCourse(course, id) + refresh() + } + func markLit(_ id: Int) { progress.markLit(id) refresh() diff --git a/ios/Graduately/Data/Content.swift b/ios/Graduately/Data/Content.swift index 86df67c..31fadc5 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -31,6 +31,12 @@ final class Content { let netTasks: [J] let netAnswers: [[J]] let hw: [J] + let net2: [J] + let net3: [J] + let net4: [J] + let hw2: [J] + let hw3: [J] + let hw4: [J] let on: [J] let on2: [J] let on3: [J] @@ -89,6 +95,12 @@ final class Content { netAnswers = [] } hw = raw.arr("hw") + net2 = raw.arr("net2") + net3 = raw.arr("net3") + net4 = raw.arr("net4") + hw2 = raw.arr("hw2") + hw3 = raw.arr("hw3") + hw4 = raw.arr("hw4") on = raw.arr("on") on2 = raw.arr("on2") on3 = raw.arr("on3") @@ -124,6 +136,20 @@ final class Content { func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } } func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } } func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } } + func itLessons(_ course: String) -> [J] { + switch course { + case "net2": return net2 + case "net3": return net3 + case "net4": return net4 + case "hw2": return hw2 + case "hw3": return hw3 + case "hw4": return hw4 + default: return [] + } + } + func itLesson(_ course: String, _ id: Int) -> J? { + itLessons(course).first { $0.int("id") == id } + } func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } } func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } } func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } } diff --git a/ios/Graduately/Data/ProgressStore.swift b/ios/Graduately/Data/ProgressStore.swift index 7f5f02d..bd1f493 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -50,6 +50,8 @@ final class ProgressStore { func markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(id)") } func deDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "de.\(year).\(id)") } func markDe(_ year: Int, _ id: Int) { defaults.set(true, forKey: "de.\(year).\(id)") } + func courseDone(_ course: String, _ id: Int) -> Bool { defaults.bool(forKey: "\(course).\(id)") } + func markCourse(_ course: String, _ id: Int) { defaults.set(true, forKey: "\(course).\(id)") } func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") } func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") } @@ -126,6 +128,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for course in ["net2", "net3", "net4", "hw2", "hw3", "hw4"] { + for l in content.itLessons(course) { + sum.totalEx += 1 + sum.openUnits += 1 + if p.courseDone(course, l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + } for l in content.on { sum.totalEx += 1 sum.openUnits += 1 diff --git a/ios/Graduately/Route.swift b/ios/Graduately/Route.swift index 74ea018..b6290a6 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -23,10 +23,28 @@ enum Route: Hashable { case netMap case netLesson(Int) case netEx(Int) + case net2Map + case net2Lesson(Int) + case net2Ex(Int) + case net3Map + case net3Lesson(Int) + case net3Ex(Int) + case net4Map + case net4Lesson(Int) + case net4Ex(Int) case hwYears case hwMap case hwLesson(Int) case hwEx(Int) + case hw2Map + case hw2Lesson(Int) + case hw2Ex(Int) + case hw3Map + case hw3Lesson(Int) + case hw3Ex(Int) + case hw4Map + case hw4Lesson(Int) + case hw4Ex(Int) case onYears case onMap case onLesson(Int) @@ -118,10 +136,28 @@ enum Route: Hashable { case .netMap: return "netmap" case .netLesson(let id): return "netunit\(id)" case .netEx(let id): return "netex\(id)" + case .net2Map: return "net2map" + case .net2Lesson(let id): return "net2unit\(id)" + case .net2Ex(let id): return "net2ex\(id)" + case .net3Map: return "net3map" + case .net3Lesson(let id): return "net3unit\(id)" + case .net3Ex(let id): return "net3ex\(id)" + case .net4Map: return "net4map" + case .net4Lesson(let id): return "net4unit\(id)" + case .net4Ex(let id): return "net4ex\(id)" case .hwYears: return "hwyears" case .hwMap: return "hwmap" case .hwLesson(let id): return "hwunit\(id)" case .hwEx(let id): return "hwex\(id)" + case .hw2Map: return "hw2map" + case .hw2Lesson(let id): return "hw2unit\(id)" + case .hw2Ex(let id): return "hw2ex\(id)" + case .hw3Map: return "hw3map" + case .hw3Lesson(let id): return "hw3unit\(id)" + case .hw3Ex(let id): return "hw3ex\(id)" + case .hw4Map: return "hw4map" + case .hw4Lesson(let id): return "hw4unit\(id)" + case .hw4Ex(let id): return "hw4ex\(id)" case .onYears: return "onyears" case .onMap: return "onmap" case .onLesson(let id): return "onunit\(id)" @@ -236,6 +272,24 @@ enum Route: Hashable { return [.netYears, .netMap, .netLesson(id)] case .netEx(let id): return [.netYears, .netMap, .netLesson(id), .netEx(id)] + case .net2Map: + return [.netYears, .net2Map] + case .net2Lesson(let id): + return [.netYears, .net2Map, .net2Lesson(id)] + case .net2Ex(let id): + return [.netYears, .net2Map, .net2Lesson(id), .net2Ex(id)] + case .net3Map: + return [.netYears, .net3Map] + case .net3Lesson(let id): + return [.netYears, .net3Map, .net3Lesson(id)] + case .net3Ex(let id): + return [.netYears, .net3Map, .net3Lesson(id), .net3Ex(id)] + case .net4Map: + return [.netYears, .net4Map] + case .net4Lesson(let id): + return [.netYears, .net4Map, .net4Lesson(id)] + case .net4Ex(let id): + return [.netYears, .net4Map, .net4Lesson(id), .net4Ex(id)] case .hwYears: return [.hwYears] case .hwMap: @@ -244,6 +298,24 @@ enum Route: Hashable { return [.hwYears, .hwMap, .hwLesson(id)] case .hwEx(let id): return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)] + case .hw2Map: + return [.hwYears, .hw2Map] + case .hw2Lesson(let id): + return [.hwYears, .hw2Map, .hw2Lesson(id)] + case .hw2Ex(let id): + return [.hwYears, .hw2Map, .hw2Lesson(id), .hw2Ex(id)] + case .hw3Map: + return [.hwYears, .hw3Map] + case .hw3Lesson(let id): + return [.hwYears, .hw3Map, .hw3Lesson(id)] + case .hw3Ex(let id): + return [.hwYears, .hw3Map, .hw3Lesson(id), .hw3Ex(id)] + case .hw4Map: + return [.hwYears, .hw4Map] + case .hw4Lesson(let id): + return [.hwYears, .hw4Map, .hw4Lesson(id)] + case .hw4Ex(let id): + return [.hwYears, .hw4Map, .hw4Lesson(id), .hw4Ex(id)] case .onYears: return [.onYears] case .onMap: @@ -403,6 +475,12 @@ func routeFromPage(_ page: String) -> Route? { case "dehome": return .deHome case "netyears": return .netYears case "netmap": return .netMap + case "net2map": return .net2Map + case "net3map": return .net3Map + case "net4map": return .net4Map + case "hw2map": return .hw2Map + case "hw3map": return .hw3Map + case "hw4map": return .hw4Map case "hwyears": return .hwYears case "hwmap": return .hwMap case "onyears": return .onYears @@ -572,6 +650,34 @@ func routeFromPage(_ page: String) -> Route? { if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) { return .deEx(y, n) } + if let m = page.wholeMatch(of: /net([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) { + switch y { + case 3: return .net3Lesson(n) + case 4: return .net4Lesson(n) + default: return .net2Lesson(n) + } + } + if let m = page.wholeMatch(of: /net([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) { + switch y { + case 3: return .net3Ex(n) + case 4: return .net4Ex(n) + default: return .net2Ex(n) + } + } + if let m = page.wholeMatch(of: /hw([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) { + switch y { + case 3: return .hw3Lesson(n) + case 4: return .hw4Lesson(n) + default: return .hw2Lesson(n) + } + } + if let m = page.wholeMatch(of: /hw([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) { + switch y { + case 3: return .hw3Ex(n) + case 4: return .hw4Ex(n) + default: return .hw2Ex(n) + } + } if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) { return .enMap(y) } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index 7027704..16db681 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -896,6 +896,62 @@ private struct MathGrid: View { } } +struct ItYearList: View { + @ObservedObject var vm: AppModel + let course: String + let titleKey: String + let subKey: String + let lesson: (Int) -> Route + + var body: some View { + LessonListScreen( + vm: vm, + title: vm.tr(titleKey), + subtitle: vm.tr(subKey), + rows: vm.content.itLessons(course).map { l in + let id = l.int("id") + return LessonRow( + id: "\(course)\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.courseDone(course, id), + destination: lesson(id) + ) + } + ) + } +} + +struct ItSlides: View { + @ObservedObject var vm: AppModel + let course: String + let id: Int + let next: Route + + var body: some View { + if let l = vm.content.itLesson(course, id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: next) + } + } +} + +struct ChoiceLessonScreen: View { + @ObservedObject var vm: AppModel + let lesson: J? + let onDone: () -> Void + + var body: some View { + if let lesson { + let spec = J([ + "type": "choice", + "title": vm.tr(lesson.str("exTitleKey")), + "sub": lesson.str("quizHeadKey"), + "questions": lesson.rawArray("quiz"), + ]) + DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey")), onDone: onDone) + } + } +} + struct FyzQuizScreen: View { @ObservedObject var vm: AppModel let year: Int diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index 0ae28e7..56b1f43 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -674,10 +674,18 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { } case "netyears": sum.totalEx = vm.content.netLessons.count + + vm.content.net2.count + vm.content.net3.count + vm.content.net4.count sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count + + ["net2", "net3", "net4"].reduce(0) { acc, course in + acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count + } case "hwyears", "hwmap": sum.totalEx = vm.content.hw.count + + vm.content.hw2.count + vm.content.hw3.count + vm.content.hw4.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count + + ["hw2", "hw3", "hw4"].reduce(0) { acc, course in + acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count + } case "enyears", "en1map", "en2map", "en3map", "en4map": sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count } sum.doneEx = (1...4).reduce(0) { acc, year in @@ -887,14 +895,23 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))") } + for course in ["net2", "net3", "net4"] { + add(vm.tr("net_year\(course.dropFirst(3))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik site") + for l in vm.content.itLessons(course) { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "site") + } + } add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik") - add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") - add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") - add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") for l in vm.content.hw { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))") } + for course in ["hw2", "hw3", "hw4"] { + add(vm.tr("hw_year\(course.dropFirst(2))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik hardware") + for l in vm.content.itLessons(course) { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "hardware") + } + } add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik") add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik") @@ -1178,16 +1195,17 @@ struct NetYearsScreen: View { var body: some View { #if os(macOS) - let keys = ["net_year1", "net_year2", "net_year3", "net_year4"] - MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in - if i == 0 { - NavigationLink(value: Route.netMap) { - MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_sub"), badge: "\(i + 1).") - } - .buttonStyle(.plain) - } else { - MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_year_locked_sub"), badge: "\(i + 1).", locked: true) + let years: [(String, String, Route)] = [ + ("net_year1", "net_sub", .netMap), + ("net_year2", "net2_sub", .net2Map), + ("net_year3", "net3_sub", .net3Map), + ("net_year4", "net4_sub", .net4Map), + ] + MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in + NavigationLink(value: years[i].2) { + MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).") } + .buttonStyle(.plain) } .navigationTitle(vm.tr("net_years_title")) .navigationSubtitle(vm.tr("net_years_sub")) @@ -1196,13 +1214,14 @@ struct NetYearsScreen: View { NavigationLink(value: Route.netMap) { ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub")) } - ForEach(["net_year2", "net_year3", "net_year4"], id: \.self) { key in - Label { - ListRowLabel(title: vm.tr(key), subtitle: vm.tr("net_year_locked_sub")) - } icon: { - Image(systemName: "lock.fill") - } - .foregroundStyle(.secondary) + NavigationLink(value: Route.net2Map) { + ListRowLabel(title: vm.tr("net_year2"), subtitle: vm.tr("net2_sub")) + } + NavigationLink(value: Route.net3Map) { + ListRowLabel(title: vm.tr("net_year3"), subtitle: vm.tr("net3_sub")) + } + NavigationLink(value: Route.net4Map) { + ListRowLabel(title: vm.tr("net_year4"), subtitle: vm.tr("net4_sub")) } } .navigationTitle(vm.tr("net_years_title")) @@ -1217,16 +1236,17 @@ struct HwYearsScreen: View { var body: some View { #if os(macOS) - let keys = ["hw_year1", "hw_year2", "hw_year3", "hw_year4"] - MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in - if i == 0 { - NavigationLink(value: Route.hwMap) { - MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_sub"), badge: "\(i + 1).") - } - .buttonStyle(.plain) - } else { - MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_year_locked_sub"), badge: "\(i + 1).", locked: true) + let years: [(String, String, Route)] = [ + ("hw_year1", "hw_sub", .hwMap), + ("hw_year2", "hw2_sub", .hw2Map), + ("hw_year3", "hw3_sub", .hw3Map), + ("hw_year4", "hw4_sub", .hw4Map), + ] + MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in + NavigationLink(value: years[i].2) { + MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).") } + .buttonStyle(.plain) } .navigationTitle(vm.tr("Technické vybavení")) .navigationSubtitle(vm.tr("hw_years_sub")) @@ -1235,13 +1255,14 @@ struct HwYearsScreen: View { NavigationLink(value: Route.hwMap) { ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub")) } - ForEach(["hw_year2", "hw_year3", "hw_year4"], id: \.self) { key in - Label { - ListRowLabel(title: vm.tr(key), subtitle: vm.tr("hw_year_locked_sub")) - } icon: { - Image(systemName: "lock.fill") - } - .foregroundStyle(.secondary) + NavigationLink(value: Route.hw2Map) { + ListRowLabel(title: vm.tr("hw_year2"), subtitle: vm.tr("hw2_sub")) + } + NavigationLink(value: Route.hw3Map) { + ListRowLabel(title: vm.tr("hw_year3"), subtitle: vm.tr("hw3_sub")) + } + NavigationLink(value: Route.hw4Map) { + ListRowLabel(title: vm.tr("hw_year4"), subtitle: vm.tr("hw4_sub")) } } .navigationTitle(vm.tr("Technické vybavení")) @@ -2256,6 +2277,24 @@ struct RouteDestination: View { } case .netEx(let id): NetQuizScreen(vm: vm, id: id) + case .net2Map: + ItYearList(vm: vm, course: "net2", titleKey: "net_year2", subKey: "net2_sub") { .net2Lesson($0) } + case .net2Lesson(let id): + ItSlides(vm: vm, course: "net2", id: id, next: .net2Ex(id)) + case .net2Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net2", id)) { vm.markCourse("net2", id) } + case .net3Map: + ItYearList(vm: vm, course: "net3", titleKey: "net_year3", subKey: "net3_sub") { .net3Lesson($0) } + case .net3Lesson(let id): + ItSlides(vm: vm, course: "net3", id: id, next: .net3Ex(id)) + case .net3Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net3", id)) { vm.markCourse("net3", id) } + case .net4Map: + ItYearList(vm: vm, course: "net4", titleKey: "net_year4", subKey: "net4_sub") { .net4Lesson($0) } + case .net4Lesson(let id): + ItSlides(vm: vm, course: "net4", id: id, next: .net4Ex(id)) + case .net4Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net4", id)) { vm.markCourse("net4", id) } case .hwYears: HwYearsScreen(vm: vm) case .hwMap: @@ -2279,6 +2318,24 @@ struct RouteDestination: View { } case .hwEx(let id): HwQuizScreen(vm: vm, id: id) + case .hw2Map: + ItYearList(vm: vm, course: "hw2", titleKey: "hw_year2", subKey: "hw2_sub") { .hw2Lesson($0) } + case .hw2Lesson(let id): + ItSlides(vm: vm, course: "hw2", id: id, next: .hw2Ex(id)) + case .hw2Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw2", id)) { vm.markCourse("hw2", id) } + case .hw3Map: + ItYearList(vm: vm, course: "hw3", titleKey: "hw_year3", subKey: "hw3_sub") { .hw3Lesson($0) } + case .hw3Lesson(let id): + ItSlides(vm: vm, course: "hw3", id: id, next: .hw3Ex(id)) + case .hw3Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw3", id)) { vm.markCourse("hw3", id) } + case .hw4Map: + ItYearList(vm: vm, course: "hw4", titleKey: "hw_year4", subKey: "hw4_sub") { .hw4Lesson($0) } + case .hw4Lesson(let id): + ItSlides(vm: vm, course: "hw4", id: id, next: .hw4Ex(id)) + case .hw4Ex(let id): + ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw4", id)) { vm.markCourse("hw4", id) } case .onYears: OnYearsScreen(vm: vm) case .enYears: diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index f776864..7972c7d 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1142,6 +1142,12 @@ def main() -> int: lit4_lessons = lit_pack("lit4") chem_lessons = lit_pack("chem") bio_lessons = lit_pack("bio") + net2_lessons = lit_pack("net2") + net3_lessons = lit_pack("net3") + net4_lessons = lit_pack("net4") + hw2_lessons = lit_pack("hw2") + hw3_lessons = lit_pack("hw3") + hw4_lessons = lit_pack("hw4") fyz_lessons = lit_pack("fyz") fyz2_lessons = lit_pack("fyz2") fyz3_lessons = lit_pack("fyz3") @@ -1692,6 +1698,12 @@ def main() -> int: "answers": net_ans(A("net_ans")), }, "hw": hw_lessons, + "net2": net2_lessons, + "net3": net3_lessons, + "net4": net4_lessons, + "hw2": hw2_lessons, + "hw3": hw3_lessons, + "hw4": hw4_lessons, "on": on_lessons, "lit": lit_lessons, "lit2": lit2_lessons, diff --git a/scripts/gen-it-years.py b/scripts/gen-it-years.py new file mode 100644 index 0000000..eb9e061 --- /dev/null +++ b/scripts/gen-it-years.py @@ -0,0 +1,2472 @@ +#!/usr/bin/env python3 +"""Years 2–4 of networks and hardware. Slides plus a four-question quiz.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def c(text): + return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def S(kicker, title, tip, *lines): + return (kicker, title, tip, lines) + + +def Q(prompt, options, correct, hint): + if len(options) != 4: + raise SystemExit(f"need 4 options: {prompt}") + if not 0 <= correct < 4: + raise SystemExit(prompt) + return (prompt, options, correct, hint) + + +def L(title, title_en, sub, sub_en, slides, quiz): + if len(slides) != 3 or len(quiz) != 4: + raise SystemExit(title) + return { + "title": title, "title_en": title_en, + "sub": sub, "sub_en": sub_en, + "slides": slides, "quiz": quiz, + } + + +def course(prefix, sub, sub_en, lessons): + if len(lessons) != 8: + raise SystemExit(f"{prefix} has {len(lessons)}") + return {"prefix": prefix, "sub": sub, "sub_en": sub_en, "lessons": lessons} + + +NET2 = course( + "net2", + "VLAN, STP, DHCP, DNS, NAT, IPv6, statické směrování a ACL.", + "VLANs, STP, DHCP, DNS, NAT, IPv6, static routing and ACLs.", + [ + L("VLAN a trunk", "VLANs and trunks", + "Logické sítě na jednom přepínači a značkovaný spoj.", + "Logical networks on one switch and a tagged link.", + [ + S("1 / 3 • VLAN", "Oddělení provozu", + "Tip: VLAN není samostatný kabel, je to značka v rámci.", + "VLAN sdružuje porty do jedné vysílací domény.", + "Počítače v různých VLAN spolu přímo na druhé vrstvě nemluví.", + "Číslo VLAN se na trunku vozí ve značce 802.1Q.", + "Nativní VLAN na trunku jede neznačkovaná. Na obou koncích musí sedět."), + S("2 / 3 • Porty", "Access a trunk", + "Tip: access port patří do jedné VLAN, trunk jich nese víc.", + "Access port se připojuje ke koncovému zařízení.", + "Trunk spojuje přepínače nebo přepínač s routerem.", + "Povolené VLAN na trunku omezí, co smí přejít.", + "Špatně nastavený trunk pustí cizí VLAN do špatné sítě."), + S("3 / 3 • Provoz", "K čemu se VLAN hodí", + "Tip: oddělení hostů od školy nesmí stát jen na hesle Wi-Fi.", + "Hosté, škola a správa mohou sdílet jeden přepínač.", + "Hlasová VLAN oddělí telefony od dat.", + "Mezi VLAN se dostane jen směrovač nebo přepínač třetí vrstvy.", + "VLAN zmenšuje broadcast, nenahrazuje firewall."), + ], + [ + Q("Co VLAN na přepínači dělá?", + ["Sdruží porty do jedné vysílací domény", "Zrychlí procesor serveru", + "Nahradí šifrování WPA3", "Přidělí veřejnou IPv4 adresu"], + 0, "VLAN odděluje provoz na druhé vrstvě."), + Q("Jak se VLAN pozná na trunku?", + ["Značkou 802.1Q v rámci", "Jinou MAC adresou zdroje", + "Jiným konektorem RJ11", "Bitem v ICMP"], + 0, "802.1Q vloží do ethernetového rámce číslo VLAN."), + Q("Kam patří access port?", + ["Ke koncovému počítači v jedné VLAN", "Jen mezi dvěma routery", + "Na optický spoj s vlnovým multiplexem", "Do napájecího zdroje"], + 0, "Access port má jednu VLAN a značku obvykle sundá."), + Q("Jak spolu mluví dvě různé VLAN?", + ["Přes směrování, ne přímo na druhé vrstvě", + "Samy od sebe, VLAN je jen barva kabelu", + "Jedině přes ARP broadcast do všech VLAN", + "Jen když mají stejnou MAC adresu"], + 0, "Mezi VLAN je potřeba router nebo L3 switch."), + ]), + L("STP a smyčky", "STP and loops", + "Proč smyčka na druhé vrstvě shodí síť a jak ji STP přeruší.", + "Why a layer-2 loop takes the network down, and how STP breaks it.", + [ + S("1 / 3 • Smyčka", "Broadcastová bouře", + "Tip: na druhé vrstvě není TTL, rámec může kroužit donekonečna.", + "Dva kabely mezi stejnými přepínači vytvoří smyčku.", + "Broadcast se kopíruje na všechny porty a vrátí se zpět.", + "Přepínač se zahltí a přestane přenášet užitečný provoz.", + "Na třetí vrstvě paket TTL snižuje, ethernetový rámec ne."), + S("2 / 3 • STP", "Jedna aktivní cesta", + "Tip: STP nevybírá nejrychlejší cestu pro data, brání smyčce.", + "Přepínače si volí root bridge, obvykle nejnižší bridge ID.", + "Některé porty zůstanou ve stavu blocking a rámce nepřeposílají.", + "Když aktivní linka spadne, blocking port se může otevřít.", + "RSTP reaguje rychleji než původní STP."), + S("3 / 3 • Ochrana", "Porty, které nemají přepínat", + "Tip: port k tiskárně nemá poslouchat cizí BPDU.", + "PortFast na access portu zrychlí připojení počítače.", + "BPDU Guard port shodí, když na něm přijde BPDU.", + "Root Guard brání tomu, aby se cizí switch stal rootem.", + "Záložní kabel má smysl, ale jen se zapnutým STP."), + ], + [ + Q("Proč je smyčka na přepínačích nebezpečná?", + ["Broadcast může kroužit bez omezení TTL", + "IPv4 přestane mít masku", + "DNS přejmenuje domény", + "Procesor v počítači se vypne"], + 0, "Ethernetový rámec nemá TTL."), + Q("Co STP s nadbytečným portem udělá?", + ["Nechá ho blokovat, aby nevznikla smyčka", + "Zdvojnásobí rychlost obou kabelů", + "Smaže MAC tabulku navždy", + "Přesměruje port na VLAN 1 a vypne ostatní"], + 0, "Blocking port rámce nepřeposílá, dokud není potřeba."), + Q("K čemu je BPDU Guard?", + ["Shodí access port, na kterém se objeví cizí přepínač", + "Šifruje heslo k Wi-Fi", + "Přiděluje IPv6 adresy", + "Měří útlum optiky"], + 0, "Na portu pro počítač nemá co dělat protokol přepínačů."), + Q("Které tvrzení o RSTP sedí?", + ["Na výpadek reaguje rychleji než původní STP", + "Ruší potřebu VLAN", + "Funguje jen na IPv6", + "Nahrazuje směrovací tabulku"], + 0, "RSTP zkracuje čekání při změně topologie."), + ]), + L("DHCP a DNS", "DHCP and DNS", + "Kdo přiděluje adresu a kdo překládá jméno.", + "Who hands out an address, and who translates a name.", + [ + S("1 / 3 • DHCP", "Adresa bez opisování", + "Tip: DHCP dává adresu na čas, ne navždy.", + "Klient pošle broadcast a hledá server.", + "Nabídka obsahuje adresu, masku, bránu a DNS.", + "Zapůjčení má dobu platnosti a klient ji obnovuje.", + "Rezervace přiváže adresu ke konkrétní MAC adrese."), + S("2 / 3 • Rozsah", "Co se nesmí překrývat", + "Tip: dva servery se stejnou adresou v jedné síti si škodí.", + "Rozsah musí ležet v síti, kterou má brána.", + "Statické adresy serverů do rozsahu nepatří, nebo se vyloučí.", + "Špatná brána v nabídce odřízne celou VLAN od internetu.", + "DHCP relay přenese požadavek z VLAN, kde server není."), + S("3 / 3 • DNS", "Jméno na adresu", + "Tip: DNS není totéž co DHCP. Jedno dává adresu, druhé jméno.", + "A záznam ukazuje na IPv4, AAAA na IPv6.", + "Reverzní PTR překládá adresu zpět na jméno.", + "Rekurzivní resolver se ptá dál, autoritativní server zónu vlastní.", + "TTL u záznamu říká, jak dlouho se smí odpověď cacheovat."), + ], + [ + Q("Co klient od DHCP obvykle dostane?", + ["Adresu, masku, bránu a DNS", "Jen heslo k Wi-Fi", + "Novou MAC adresu", "Kořenový certifikát webu"], + 0, "To jsou čtyři údaje, bez kterých stanice v síti nepracuje."), + Q("Proč se rezervace v DHCP hodí?", + ["Zařízení s danou MAC dostane pořád stejnou adresu", + "Vypne potřebu masky", + "Nahradí směrování", + "Zašifruje disk"], + 0, "Rezervace je vhodná pro tiskárny a kamery."), + Q("Čím se liší A a AAAA?", + ["A je IPv4, AAAA je IPv6", "A je pošta, AAAA je web", + "A je jméno VLAN, AAAA je číslo portu", + "Oba ukazují jen na IPv4"], + 0, "Čtyři A v názvu připomínají delší IPv6 adresu."), + Q("K čemu je DHCP relay?", + ["Pošle požadavek z jiné VLAN k serveru", + "Překládá jména bez DNS", + "Blokuje STP", + "Mění rychlost portu na 10 Gb/s"], + 0, "Broadcast DHCP jinak VLAN neopustí."), + ]), + L("NAT a PAT", "NAT and PAT", + "Jedna veřejná adresa pro mnoho počítačů.", + "One public address for many computers.", + [ + S("1 / 3 • NAT", "Soukromé adresy ven", + "Tip: 192.168.0.0/16 a 10.0.0.0/8 na internetu směrovat nejdou.", + "NAT přepíše zdrojovou adresu při odchodu z lokální sítě.", + "Zpět router podle tabulky překladů provoz vrátí.", + "Zvenku nikdo sám od sebe spojení dovnitř nenaváže.", + "To je vedlejší efekt, ne náhrada firewallu."), + S("2 / 3 • PAT", "Mnoho počítačů, jedna adresa", + "Tip: PAT si pamatuje i číslo portu, ne jen adresu.", + "Více vnitřních adres sdílí jednu veřejnou.", + "Rozliší je zdrojový port.", + "Domácí router dělá téměř vždy PAT, ne čistý NAT jedna ku jedné.", + "Dojdou-li porty, nová spojení se neotevřou."), + S("3 / 3 • Dovnitř", "Když má server být vidět", + "Tip: přesměrování portu otevře jen tu službu, kterou opravdu chcete.", + "Port forward pošle příchozí port na vnitřní adresu.", + "Na serveru pak musí poslouchat daná služba a firewall ji pustit.", + "DMZ na domácím routeru vystaví jeden počítač hodně otevřeně.", + "Lepší je konkrétní port než celá DMZ."), + ], + [ + Q("Proč stanice s adresou 192.168.1.20 není z internetu vidět přímo?", + ["Je to soukromá adresa a ven jde až přes NAT", + "IPv4 takové adresy nezná", + "MAC adresa se na internetu nesmí použít jako jméno", + "DNS soukromé adresy maže"], + 0, "Soukromé rozsahy se mezi poskytovateli nesměrují."), + Q("Čím se PAT liší od NAT jedna ku jedné?", + ["Více vnitřních adres sdílí jednu veřejnou a liší se portem", + "PAT šifruje provoz", + "PAT funguje jen pro IPv6", + "PAT ruší potřebu brány"], + 0, "Překlad zahrnuje adresu i port."), + Q("K čemu je přesměrování portu?", + ["Příchozí spojení na daný port pošle na vnitřní server", + "Přidělí VLAN", + "Zapne STP", + "Změní masku celé školy na /8"], + 0, "Bez něj zvenku na vnitřní web sami nedosáhnete."), + Q("Proč NAT není firewall?", + ["Jen překládá adresy, pravidla provozu řeší firewall", + "NAT umí totéž co antivir", + "NAT kontroluje hesla uživatelů", + "NAT šifruje Wi-Fi"], + 0, "Nechtěný publikovaný port NAT klidně pustí."), + ]), + L("IPv6 základy", "IPv6 basics", + "Delší adresa, jiný zápis a co se nemění.", + "A longer address, a different notation, and what stays the same.", + [ + S("1 / 3 • Adresa", "128 bitů", + "Tip: nuly v jedné skupině za sebou smí zkrátit :: jen jednou.", + "Adresa má osm skupin po čtyřech hexadecimálních číslicích.", + "Prefix se píše jako /64, ne jako stará maska 255.255.255.0.", + "Link-local adresa fe80:: platí jen na tom spoji.", + "Globální adresa je vidět i mimo místní spoj."), + S("2 / 3 • Přidělení", "SLAAC a DHCPv6", + "Tip: výchozí bránu v IPv6 často ohlašuje router, ne DHCP.", + "Router advertisement řekne prefix a jestli se stanice smí nastavit sama.", + "SLAAC složí adresu z prefixu a identifikátoru rozhraní.", + "DHCPv6 přidá adresu nebo jen DNS, podle příznaků.", + "Obě varianty mohou běžet vedle sebe."), + S("3 / 3 • Vedle IPv4", "Dual stack", + "Tip: IPv6 nevypínejte jen proto, že mu nerozumíte. Rozbitý IPv6 zpomalí i weby.", + "Počítač může mít IPv4 i IPv6 naráz.", + "DNS AAAA záznam rozhoduje, jestli se klient zkusí spojit po IPv6.", + "NAT u IPv6 není nutný tak jako u IPv4, ale firewall ano.", + "Ping na IPv6 je jiný příkaz než ping na IPv4, podle systému."), + ], + [ + Q("Jak je IPv6 adresa dlouhá?", + ["128 bitů", "32 bitů", "48 bitů", "256 bitů"], + 0, "IPv4 má 32 bitů, IPv6 čtyřikrát tolik."), + Q("Která adresa je link-local?", + ["fe80::1", "192.168.1.1", "8.8.8.8", "255.255.255.255"], + 0, "fe80:: platí jen na místním spoji."), + Q("Kdo v IPv6 často oznámí prefix?", + ["Router advertisement", "ARP request", + "STP hello", "Jen ruční zápis do hosts"], + 0, "Router říká stanicím, jaký prefix na spoji platí."), + Q("Co znamená dual stack?", + ["Zařízení používá IPv4 i IPv6", + "Dva switche v jedné VLAN", + "Dvě masky u jedné IPv4 adresy", + "Záloha jen na pásku"], + 0, "Oba protokoly běží současně."), + ]), + L("Statické směrování", "Static routing", + "Ruční cesta, metrika a kdy přestane stačit.", + "A hand-written route, the metric, and when it is no longer enough.", + [ + S("1 / 3 • Trasa", "Kam s paketem", + "Tip: směrovací tabulka se dívá na síť, ne na jednu adresu stanice.", + "Záznam říká síť, masku nebo prefix a další skok.", + "Další skok musí být dosažitelný na přímo připojeném rozhraní.", + "Výchozí trasa 0.0.0.0/0 chytí vše, co nemá přesnější záznam.", + "Přesnější prefix vyhraje nad kratším."), + S("2 / 3 • Metrika", "Když je cest víc", + "Tip: nižší administrativní vzdálenost znamená důvěryhodnější zdroj trasy.", + "Statická trasa má lepší vzdálenost než OSPF nebo RIP.", + "Metrika porovnává trasy ze stejného zdroje.", + "Plovoucí statická trasa má horší vzdálenost a čeká jako záloha.", + "Špatný další skok vytvoří černou díru: paket odejde a ztratí se."), + S("3 / 3 • Meze", "Kdy ruční tabulka nestačí", + "Tip: deset poboček se statickými trasami neudržíte po každé změně.", + "Statika je přehledná v malé síti a na okraji k poskytovateli.", + "Neumí sama obejít spadlou cestu, pokud záloha není připravená.", + "Na směrovači musí sedět trasa tam i zpět, jinak odpověď nepřijde.", + "Změna adresace znamená obejít všechny ruční záznamy."), + ], + [ + Q("Na co se směrovač dívá při výběru trasy?", + ["Na síť a prefix, ne na jméno počítače", + "Na barvu kabelu", + "Na VLAN jméno v DNS", + "Jen na MAC adresu cíle"], + 0, "Třetí vrstva směruje podle prefixu."), + Q("Která trasa je výchozí?", + ["0.0.0.0/0", "255.255.255.255/32", "192.168.1.1/32", "::1/128"], + 0, "Nulová síť s nulovým prefixem chytí zbytek."), + Q("Co je plovoucí statická trasa?", + ["Záložní ruční trasa s horší vzdáleností", + "Trasa, která plave mezi VLAN bez routeru", + "Automatický OSPF", + "Záznam jen v ARP"], + 0, "Nastoupí, až když zmizí lepší cesta."), + Q("Proč odpověď ze serveru nedorazí, i když tam paket došel?", + ["Chybí zpáteční trasa", + "TCP nemá porty", + "IPv4 neumí odpovědi", + "Switch maže cílové MAC adresy"], + 0, "Cesta musí existovat oběma směry."), + ]), + L("ACL a filtrování", "ACLs and filtering", + "Seznam, který provoz pustí a který zahodí.", + "A list that permits traffic or drops it.", + [ + S("1 / 3 • Pravidla", "Pořadí rozhoduje", + "Tip: první shoda vyhrává. Obecné povolení na začátku pozdější zákaz schová.", + "ACL zkouší řádky shora dolů.", + "Na konci bývá tiché zamítnutí všeho ostatního.", + "Pravidlo umí adresu, protokol a port.", + "Jedno špatné pořadí otevře službu, kterou jste chtěli zavřít."), + S("2 / 3 • Směr", "Dovnitř a ven", + "Tip: blíž zdroji filtrujte to, co nemá vůbec vstoupit.", + "Příchozí ACL sedí na paketech, které do rozhraní vcházejí.", + "Odchozí ACL sedí na paketech, které rozhraní opouštějí.", + "Filtr na VLAN rozhraní hlídá provoz mezi sítěmi.", + "ACL na směrovači nevidí provoz, který zůstane uvnitř jednoho switche."), + S("3 / 3 • Praxe", "Co obvykle pustit", + "Tip: nejdřív povolte, co potřebujete, a zbytek nechte spadnout.", + "Správa směrovače nemá poslouchat z hostovské VLAN.", + "Do serverové sítě pusťte jen porty služeb, které tam jsou.", + "ICMP můžete omezit, ale úplný zákaz ztíží hledání chyb.", + "Po změně ACL ověřte, že legitimní provoz pořád prochází."), + ], + [ + Q("Jak ACL vybírá pravidlo?", + ["Použije první řádek, který sedí", + "Sečte všechny řádky a udělá průměr", + "Vždy použije poslední řádek", + "Ptá se DNS"], + 0, "Pořadí je součást významu seznamu."), + Q("Co udělá tiché zamítnutí na konci?", + ["Zahodí provoz, který žádný řádek nepovolil", + "Povolí všechno, co zbylo", + "Přepne port do trunku", + "Pošle paket do IPv6"], + 0, "Co není povoleno, neprojde."), + Q("Proč ACL na routeru nehlídá dva počítače v jedné VLAN?", + ["Jejich rámce jdou jen přes switch a k routeru nemusí", + "ACL umí jen Wi-Fi", + "VLAN nemá MAC adresy", + "TCP takový provoz šifruje před switchem"], + 0, "Provoz uvnitř VLAN zůstane na druhé vrstvě."), + Q("Kam nepatří správa routeru?", + ["Do hostovské sítě", "Na konzolový port technika", + "Do správní VLAN", "Na šifrované SSH z dohledu"], + 0, "Host nemá mít cestu na správu infrastruktury."), + ]), + L("Podniková Wi-Fi", "Enterprise Wi-Fi", + "SSID, WPA3 a proč jedno heslo na nástěnce nestačí.", + "SSIDs, WPA3, and why one password on the wall is not enough.", + [ + S("1 / 3 • SSID", "Více sítí, jedna infrastruktura", + "Tip: hostovské SSID má skončit v jiné VLAN než učitelé.", + "Jedno rádio může ohlašovat víc SSID.", + "Každé SSID se mapuje na VLAN.", + "Schované SSID není zabezpečení, jméno jde pořád zachytit.", + "Stejné SSID na více přístupových bodech umožní přechod."), + S("2 / 3 • Klíče", "WPA2 a WPA3", + "Tip: společné heslo PSK se nedá vzít jednomu člověku, aniž by ho dostali všichni.", + "WPA2-Personal sdílí jedno heslo.", + "WPA2-Enterprise ověřuje uživatele přes RADIUS.", + "WPA3 lépe chrání i slabší hesla a má lepší dopředné utajení.", + "Starý WEP se nepoužívá."), + S("3 / 3 • Pokrytí", "Kanály a přechod", + "Tip: dva sousední body na stejném kanále si překážejí.", + "Pásmo 2,4 GHz má málo nepřekrývajících se kanálů.", + "5 GHz má víc kanálů a hůř prochází zdí.", + "Výkon nesmí být zbytečně vysoký, jinak buňky slyší i cizí provoz.", + "Roaming funguje, když se SSID a zabezpečení shodují."), + ], + [ + Q("Kam má vést hostovské SSID?", + ["Do vlastní VLAN, oddělené od školní sítě", + "Do stejné sítě jako ředitelna", + "Přímo do správy routeru", + "Do sítě bez DHCP i bez izolace"], + 0, "Host nemá vidět školní servery."), + Q("Čím se liší WPA2-Enterprise od společného hesla?", + ["Každý uživatel se ověřuje zvlášť, často přes RADIUS", + "Nemá šifrování", + "Funguje jen na 2,4 GHz", + "Sdílí jeden klíč napsaný na tabuli"], + 0, "Účet jde odebrat jednomu člověku."), + Q("Proč schované SSID není ochrana?", + ["Jméno sítě jde při připojení stejně zachytit", + "Skryté SSID nejde vůbec použít", + "WPA3 ho zakazuje", + "Switch ho vyhlásí v STP"], + 0, "Utajené jméno není šifra."), + Q("Co vadí dvěma sousedním AP na stejném kanále?", + ["Slyší se a kazí si přenos", + "Smažou si VLAN", + "Přestanou dělat DHCP", + "Změní IPv6 na IPv4"], + 0, "Kanály se mají plánovat, ne nechat náhodě."), + ]), + ], +) + +NET3 = course( + "net3", + "OSPF, směrování mezi VLAN, WAN, VPN, firewall, QoS a dohled.", + "OSPF, inter-VLAN routing, WAN, VPN, firewalls, QoS and monitoring.", + [ + L("OSPF", "OSPF", + "Směrovače si samy řeknou, jaké sítě znají.", + "Routers tell each other which networks they know.", + [ + S("1 / 3 • Sousedé", "Stejná oblast", + "Tip: OSPF soused nevznikne, když se neshoduje oblast nebo hello.", + "Směrovače v jednom spoji si vymění hello.", + "Hello interval a dead interval musí sedět.", + "Oblast 0 je páteř. Ostatní oblasti se k ní připojují.", + "Pasivní rozhraní hello neposílá, třeba k počítačům."), + S("2 / 3 • Cena", "Kratší neznamená míň skoků", + "Tip: OSPF sčítá cenu linek, ne počet routerů jako RIP.", + "Rychlejší linka má obvykle nižší cenu.", + "Součet cen rozhodne, kudy paket půjde.", + "Ruční cena umí odtáhnout provoz ze špatné linky.", + "Změna topologie se přepočítá bez přepisování statických tras."), + S("3 / 3 • Tabulka", "Co se z OSPF stane", + "Tip: v tabulce uvidíte zdroj trasy, ne jen další skok.", + "Naučená síť se vloží do směrovací tabulky.", + "Když lepší cesta spadne, použije se náhradní.", + "Redistribuce natáhne do OSPF i statiku nebo jiný protokol.", + "Špatná redistribuce umí do sítě pustit cizí trasy."), + ], + [ + Q("Co musí u OSPF sousedů sedět?", + ["Oblast a časy hello", "Stejná MAC adresa", + "Stejné SSID", "Stejný počet VLAN na switchi"], + 0, "Nesoulad hello nebo oblasti sousedství nesestaví."), + Q("Podle čeho OSPF vybírá cestu?", + ["Podle součtu cen linek", "Podle počtu písmen v názvu routeru", + "Podle VLAN ID", "Podle stáří DNS záznamu"], + 0, "Cena vyjadřuje náklad cesty, často odvozený z rychlosti."), + Q("K čemu je pasivní rozhraní?", + ["Neposílá hello tam, kde žádný soused být nemá", + "Vypne směrování celé sítě", + "Zapne NAT", + "Skryje SSID"], + 0, "K uživatelům se hello zbytečně neposílá."), + Q("Co je oblast 0?", + ["Páteř, ke které se ostatní oblasti připojují", + "VLAN pro hosty", + "Výchozí heslo WPA", + "Číslo konzolového portu"], + 0, "Bez páteře se víc oblastí OSPF nespojí správně."), + ]), + L("Směrování mezi VLAN", "Inter-VLAN routing", + "Jedna brána pro každou VLAN.", + "One gateway for each VLAN.", + [ + S("1 / 3 • Router na tyči", "Jeden spoj, víc VLAN", + "Tip: trunk k routeru nese značky a každá VLAN má své podrozhraní.", + "Switch pošle trunk do routeru.", + "Každá VLAN má vlastní IP bránu.", + "Stanice má jako bránu adresu své VLAN, ne cizí.", + "Bez trasy mezi VLAN k sobě sítě nedosáhnou."), + S("2 / 3 • L3 switch", "Směrování v přepínači", + "Tip: přepínač třetí vrstvy má pro VLAN rozhraní SVI.", + "SVI je virtuální rozhraní s adresou brány.", + "Provoz mezi VLAN nemusí odejít na samostatný router.", + "ACL na SVI hlídá, která VLAN kam smí.", + "Zapomenuté SVI znamená, že VLAN nemá bránu."), + S("3 / 3 • Chyby", "Když to nejde", + "Tip: nejdřív ověřte, že stanice je ve správné VLAN a má správnou bránu.", + "Špatná maska na bráně rozhodí celou VLAN.", + "Trunk, který VLAN nepropouští, bránu odstřihne.", + "Stanice v access portu špatné VLAN dostane cizí DHCP.", + "Kontrola: adresa, maska, brána, VLAN, trunk, ACL."), + ], + [ + Q("Co je brána pro stanici ve VLAN?", + ["Adresa routeru nebo SVI v té samé VLAN", + "Libovolná veřejná DNS adresa", + "MAC adresa switche", + "Číslo VLAN zapsané do masky"], + 0, "Brána musí být dosažitelná na druhé vrstvě, tedy ve stejné VLAN."), + Q("Co je SVI?", + ["Virtuální rozhraní VLAN na L3 přepínači", + "Typ optického vlákna", + "Protokol pošty", + "Záloha konfigurace"], + 0, "SVI nese IP adresu brány dané VLAN."), + Q("Jak router na tyči pozná VLAN?", + ["Podle značky 802.1Q na trunku", + "Podle barvy LED na počítači", + "Podle jména v DNS", + "Podle rychlosti ventilátoru"], + 0, "Každé podrozhraní patří jedné značce."), + Q("Co udělá trunk, který VLAN 20 nepropouští?", + ["Střihne bránu VLAN 20 od zbytku sítě", + "Smaže OSPF v celé škole", + "Přepne Wi-Fi na WEP", + "Zdvojí adresy DHCP"], + 0, "Povolené VLAN na trunku jsou součást návrhu."), + ]), + L("WAN", "WAN", + "Spoj mezi pobočkami a k poskytovateli.", + "A link between sites and towards the provider.", + [ + S("1 / 3 • Okraj", "Kde končí vaše síť", + "Tip: za routerem k poskytovateli už neřídíte fyzickou cestu.", + "WAN spojuje vzdálené sítě přes síť, kterou nevlastníte.", + "Poskytovatel vám dá adresu, bránu a často i VLAN.", + "Výchozí trasa obvykle míří právě sem.", + "Výpadek poslední míle odřízne pobočku, i když jádro žije."), + S("2 / 3 • Technologie", "Čím se to vozí", + "Tip: dnešní WAN často vypadá jako ethernet, i když pod ním je jiná síť.", + "Starší spoje používaly pronajaté linky a PPP.", + "MPLS u poskytovatele spojí pobočky jako jednu službu.", + "Metro Ethernet přivede do budovy běžný ethernetový port.", + "Rychlost, odezva a ztráty jsou parametry, které se sledují."), + S("3 / 3 • Návrh", "Co domluvit", + "Tip: záložní spoj má smysl jen s cestou, která se na něj umí přepnout.", + "Jedna přípojka je jednoduchá a křehká.", + "Druhý poskytovatel sníží riziko, když nejde stejnou trasou.", + "Adresy od poskytovatele musí sedět s vaším NAT a firewallem.", + "Na okraji filtrujte, co do školy z internetu lézt nemá."), + ], + [ + Q("Co je WAN z pohledu školy?", + ["Spoj mimo budovu, často přes poskytovatele", + "Kabel mezi dvěma porty jednoho switche", + "VLAN pro tiskárny", + "Diskové pole"], + 0, "WAN překračuje místní síť."), + Q("Kam obvykle míří výchozí trasa?", + ["K poskytovateli", "Na tiskárnu", + "Do STP rootu", "Na adresu 127.0.0.1"], + 0, "Co směrovač nezná, pošle ven."), + Q("Proč dva spoje stejnou ulicí nemusí být záloha?", + ["Jedna porucha překope oba naráz", + "OSPF dva spoje zakazuje", + "IPv4 umí jen jednu bránu na světě", + "Ethernet nesmí mít zálohu"], + 0, "Fyzicky oddělená trasa je součást redundance."), + Q("Co je metro Ethernet?", + ["WAN služba, která u vás končí ethernetovým portem", + "Wi-Fi ve vlaku", + "Protokol místo DNS", + "Typ procesoru"], + 0, "Pod portem může být síť poskytovatele, vy vidíte ethernet."), + ]), + L("VPN", "VPN", + "Šifrovaný tunel přes síť, které nevěříte.", + "An encrypted tunnel across a network you do not trust.", + [ + S("1 / 3 • Tunel", "Cizí síť jako kabel", + "Tip: VPN neschová provoz před sítí, ve které tunel končí.", + "Mezi dvěma body se postaví šifrovaný tunel.", + "Uvnitř mohou téct soukromé adresy.", + "Venku je vidět jen obal mezi konci tunelu.", + "Bez shodných klíčů nebo certifikátů tunel nevstane."), + S("2 / 3 • Dva tvary", "Pobočka a člověk", + "Tip: site-to-site spojuje sítě, remote access jednoho uživatele.", + "Site-to-site propojí dvě pobočky trvale.", + "Remote access pustí notebook do školní sítě.", + "Uživatel se ověřuje účtem, ne jen společným heslem na zdi.", + "Split tunnel posílá tunelem jen školní sítě, zbytek jde přímo."), + S("3 / 3 • Chyby", "Když tunel stojí a data ne", + "Tip: tunel může svítit zeleně a přesto chybí trasa dovnitř.", + "Musí sedět návrh šifry na obou koncích.", + "Firewall po cestě musí pustit port VPN.", + "Uvnitř musí existovat trasa do cílových sítí.", + "Příliš široký tunel stáhne na školu i provoz, který tam být neměl."), + ], + [ + Q("Co VPN před cizí sítí skrývá?", + ["Obsah a vnitřní adresy, venku je vidět obal tunelu", + "I to, že nějaký provoz existuje", + "MAC adresu switche ve třídě", + "Úplně všechno včetně konců tunelu"], + 0, "Koncové adresy tunelu vidět jsou, náklad ne."), + Q("Čím se liší site-to-site od remote access?", + ["První spojuje sítě, druhá připojuje uživatele", + "První je Wi-Fi, druhá je VLAN", + "Obě jen mění DNS", + "Remote access nejde šifrovat"], + 0, "Notebook není druhá pobočka, i když tunel vypadá podobně."), + Q("Proč tunel svítí, ale server nehraje?", + ["Chybí trasa nebo firewall uvnitř", + "IPv4 zakazuje VPN", + "STP tunely maže", + "DNS přejmenuje šifru"], + 0, "Šifra a dosažitelnost jsou dvě vrstvy."), + Q("Co je split tunnel?", + ["Tunelem jde jen provoz do vnitřních sítí", + "Každý paket se rozpůlí", + "VPN bez šifry", + "Dva SSID na jednom kanále"], + 0, "Internet z notebooku pak nemusí téct přes školu."), + ]), + L("Firewall a zóny", "Firewalls and zones", + "Kdo s kým smí mluvit, ne jen jestli kabel svítí.", + "Who may talk to whom, not only whether the cable is lit.", + [ + S("1 / 3 • Zóny", "Důvěra není stejná", + "Tip: LAN, WAN a DMZ nemají stejná pravidla.", + "Rozhraní se sdruží do zón podle důvěry.", + "Z důvěryhodnější zóny ven se často povoluje snáz.", + "Z internetu dovnitř se povoluje jen to, co je schválené.", + "Pravidlo se píše od zóny ke zóně, ne jen na jednu adresu."), + S("2 / 3 • Stav", "Pamatuje si spojení", + "Tip: stavový firewall pustí odpověď, aniž byste psali pravidlo nazpátek.", + "Nové spojení se kontroluje proti politice.", + "Odpověď patřící ke spojení se pustí.", + "Bezestavový filtr zkoumá každý paket zvlášť.", + "Ukončené spojení se z tabulky smaže."), + S("3 / 3 • DMZ", "Server, který má být vidět", + "Tip: web v DMZ nemá mít cestu ke všem diskům ve škole.", + "Veřejná služba sedí v oddělené zóně.", + "Z internetu se otevře jen její port.", + "Z DMZ do LAN se pustí jen to, co služba opravdu potřebuje.", + "Kompromitovaný web pak neotevře celou školu."), + ], + [ + Q("Proč se zóny liší?", + ["Mají jinou důvěru a tedy jiná pravidla", + "Každá zóna musí mít vlastní DNS jméno switche", + "Zóny nahrazují IP adresy", + "Bez zón nejde STP"], + 0, "Internet a učebna nejsou stejně důvěryhodné."), + Q("Co umí stavový firewall?", + ["Pustí odpověď k už otevřenému spojení", + "Smaže VLAN", + "Přidělí MAC adresu", + "Nahradí směrovací protokol"], + 0, "Pamatuje si, kdo spojení začal."), + Q("Kam patří veřejný web, který nemá vidět celou LAN?", + ["Do DMZ", "Do stejné sítě jako účtárna", + "Na access port ředitele", "Do nativní VLAN trunku bez pravidel"], + 0, "DMZ omezí, kam se útočník dostane dál."), + Q("Co samotná zóna ještě není?", + ["Náhrada za konkrétní pravidla služeb", + "Způsob, jak sdružit rozhraní", + "Hranice důvěry", + "Místo, kam se píše politika"], + 0, "Zóna bez pravidel nic nechrání."), + ]), + L("QoS", "QoS", + "Když je linka plná, někdo musí dostat přednost.", + "When the link is full, someone has to go first.", + [ + S("1 / 3 • Fronty", "Ne všechno snese čekání", + "Tip: QoS nevytvoří rychlost, jen rozhodne, kdo počká.", + "Hlas a videohovor kazí zpoždění a kolísání.", + "Záloha disku snese čekání líp než telefonát.", + "Fronta na úzké lince řadí pakety.", + "Bez označení provozu switch neví, co je hlas."), + S("2 / 3 • Značky", "Kdo si řekne o přednost", + "Tip: značce z koncového portu nevěřte, uživatel si ji může nastavit sám.", + "IP hlavička umí nést DSCP.", + "Ethernet umí prioritu na VLAN značce.", + "Na vstupu se značka přepíše podle politiky.", + "Hlasová VLAN se často značí automaticky."), + S("3 / 3 • Praxe", "Kde to má smysl", + "Tip: na prázdné gigabitové lince QoS nic viditelného neudělá.", + "Úzké hrdlo bývá WAN, ne páteřní switch.", + "Rezervace pro hlas nesmí sežrat celou linku.", + "Testujte hovor ve chvíli, kdy běží velká kopie.", + "Špatná politika umí zpomalit i správu sítě."), + ], + [ + Q("Co QoS neumí?", + ["Vyrobit větší rychlost linky", + "Dát přednost hlasu před zálohou", + "Omezit kolísání hovoru", + "Označit provoz"], + 0, "Dělí existující kapacitu, nepřidá ji."), + Q("Proč se značka z počítače přepisuje?", + ["Uživatel by si jinak dal nejvyšší prioritu", + "DSCP nejde číst", + "IPv4 značky zakazuje", + "STP značky maže"], + 0, "Důvěryhodná značka vznikne na vstupu podle pravidel."), + Q("Kde QoS obvykle pomůže nejdřív?", + ["Na pomalé WAN lince", "Na prázdném 10Gb portu mezi switchi", + "Uvnitř jednoho počítače na smyčce", "Na vypnutém Wi-Fi"], + 0, "Přednost se projeví, až když se linka plní."), + Q("Který provoz čekání snese hůř?", + ["Telefonní hovor", "Noční kopie disku", + "Stahování aktualizace", "Archiv pošty"], + 0, "Hlas potřebuje malé a stálé zpoždění."), + ]), + L("Dohled sítě", "Watching the network", + "SNMP, syslog a co z grafu opravdu plyne.", + "SNMP, syslog, and what a graph really tells you.", + [ + S("1 / 3 • SNMP", "Čísla z zařízení", + "Tip: SNMPv2c posílá jméno komunity skoro jako heslo v čistém textu.", + "Dohled se ptá na vytížení, chyby a stav portů.", + "Komunita není ozdoba, je to přístup.", + "SNMPv3 umí uživatele a šifrování.", + "Zařízení má poslouchat SNMP jen ze sítě dohledu."), + S("2 / 3 • Syslog", "Věty o tom, co se stalo", + "Tip: log na zařízení se při restartu může ztratit. Posílejte ho pryč.", + "Syslog odnáší hlášky na server.", + "Úrovně od ladění po nouzi se nemají všechny sypat do jedné hromady.", + "Čas na zařízeních musí sedět, jinak nejde srovnat události.", + "NTP je proto součást dohledu, ne jen hodiny na zdi."), + S("3 / 3 • Čtení", "Graf není diagnóza", + "Tip: špička jednou za den může být záloha, ne útok.", + "Chyby na portu často znamenají kabel nebo duplex.", + "Vytrvalé vytížení WAN řekne, jestli je čas na silnější spoj.", + "Výpadek, který nikdo nevidí, se bude opakovat.", + "Upozornění má chodit člověku, který může reagovat."), + ], + [ + Q("Proč SNMPv2c nestačí na nezabezpečené síti?", + ["Jméno komunity jde odposlechnout", + "Neumí číst vytížení", + "Funguje jen na IPv6", + "Maže syslog"], + 0, "Komunita cestuje bez pořádné ochrany. Lepší je SNMPv3."), + Q("Proč posílat syslog pryč ze zařízení?", + ["Lokální log se při restartu může ztratit", + "Syslog nahrazuje heslo", + "Switch jinak vypne porty", + "DNS bez logu nefunguje"], + 0, "Po restartu už příčinu pádu na zařízení nemusíte najít."), + Q("K čemu je při dohledu NTP?", + ["Aby časy událostí šly srovnat", + "Aby přidělil VLAN", + "Aby šifroval Wi-Fi", + "Aby nahradil SNMP"], + 0, "Bez společného času nejde říct, co bylo dřív."), + Q("Co často znamenají rostoucí chyby na portu?", + ["Špatný kabel nebo duplex", + "Že je VLAN správně", + "Že OSPF konverguje", + "Že DNS TTL vypršelo"], + 0, "Fyzická vrstva se v počítadlech chyb projeví dřív než v teorii."), + ]), + L("Záloha konfigurace", "Configuration backups", + "Co uložit, než se přepínač vymění.", + "What to save before a switch is replaced.", + [ + S("1 / 3 • Běžící a uložená", "Restart není uložení", + "Tip: změna v běžící konfiguraci po restartu zmizí, dokud ji neuložíte.", + "Běžící konfigurace je to, co zařízení právě dělá.", + "Startovní se načte po zapnutí.", + "Uložení zkopíruje běžící do startovní.", + "Bez uložení druhý den síť vypadá jako včera."), + S("2 / 3 • Kopie ven", "Nejen na zařízení", + "Tip: záloha na tom samém přepínači shoří spolu s ním.", + "Konfigurace patří na server mimo to zařízení.", + "Ukládejte i verzi, ať víte, co se změnilo.", + "Hesla v exportu mohou být čitelná. Soubor chraňte.", + "Automatická záloha po změně porazí dobrý úmysl."), + S("3 / 3 • Popis", "Aby to přečetl i kolega", + "Tip: port bez popisu je za půl roku hádanka.", + "Popis portu říká, co je na druhém konci.", + "Schéma VLAN a adres má sedět s konfigurací.", + "Změna bez poznámky se špatně vrací.", + "Nový switch postavíte z zálohy jen tehdy, když záloha existuje."), + ], + [ + Q("Co se stane se změnou, kterou neuložíte?", + ["Po restartu zmizí", "Zapíše se do VLAN navždy", + "Přepíše firmware", "Pošle se sama do DNS"], + 0, "Běžící konfigurace není totéž co startovní."), + Q("Proč záloha jen na tom samém switchi nestačí?", + ["S poškozeným switchem zmizí i ona", + "Switch zálohy šifruje tak, že je nečitelné i pro vás", + "STP zálohy maže", + "IPv6 zálohy zakazuje"], + 0, "Kopie musí žít jinde."), + Q("Co má být u portu napsané?", + ["Co je na druhém konci", "Heslo administrátora", + "Celá směrovací tabulka", "Soukromý klíč VPN"], + 0, "Popis portu šetří hodiny při poruše."), + Q("Proč chránit soubor se zálohou?", + ["Může obsahovat čitelná hesla", + "Soubor sám otevírá porty", + "DNS ho publikuje", + "Je větší než disk"], + 0, "Export není vždy v zašifrované podobě."), + ]), + ], +) + +NET4 = course( + "net4", + "Návrh adres, redundance, 802.1X, hledání chyb, cloud a maturita.", + "Address plans, redundancy, 802.1X, troubleshooting, cloud and the exam.", + [ + L("Adresní plán", "An address plan", + "Sítě se navrhují dřív, než se zapojí první kabel.", + "Networks are designed before the first cable is patched.", + [ + S("1 / 3 • Rozsahy", "Každá VLAN má svůj prefix", + "Tip: nenechávejte v každé síti jen čtyři volné adresy.", + "Nejdřív spočítejte stanice, tiskárny, AP a rezervu.", + "Brána, síťová a broadcast adresa se do stanic nepočítají.", + "Sousední VLAN nemají mít překryv.", + "Dokument říká, která čísla jsou volná."), + S("2 / 3 • Členění", "Škola není jedna velká síť", + "Tip: /16 na celou budovu se snadno spravuje a špatně izoluje.", + "Učebny, správa, servery, hosté a telefony patří odděleně.", + "Stejný řád čísel ve všech budovách se líp pamatuje.", + "Souhrn tras na páteři zmenší tabulku.", + "Výjimka pro jednu tiskárnu se má zapsat, ne jen zapamatovat."), + S("3 / 3 • Změna", "Plán, který jde rozšířit", + "Tip: když přidáte budovu, neměli byste přečíslovat celou školu.", + "Nechte v páteřním rozsahu díry pro další sítě.", + "Přechod na nový prefix znamená DHCP, DNS, brány i firewall.", + "IPv6 prefix se plánuje stejně vědomě, ne jako dodatek.", + "Tabulka adres je součást maturity i běžného provozu."), + ], + [ + Q("Co se v síti /24 nedá přiřadit stanici?", + ["Adresa sítě a broadcast", "První použitelná adresa", + "Adresa brány, pokud ji stanice nemá mít", + "Adresa s lichým posledním oktetem"], + 0, "Krajní adresy mají zvláštní význam. Brána se stanicím také nedává dvakrát."), + Q("Proč nerozdat celé škole jednu velkou síť?", + ["Špatně se izoluje porucha i provoz", + "IPv4 to zakazuje", + "Switch umí jen 8 adres", + "DNS pak nejde spustit"], + 0, "Menší sítě se líp hlídají a míň se ruší broadcastem."), + Q("K čemu je souhrn tras?", + ["Jedna trasa pokryje víc menších sítí", + "Smaže VLAN", + "Nahradí NAT", + "Zapne WPA3"], + 0, "Páteř nemusí znát každou učebnu zvlášť."), + Q("Co patří do adresního plánu?", + ["Prefix, VLAN, brána a k čemu síť je", + "Jen heslo Wi-Fi", + "Typ procesoru serveru", + "Seznam učitelů"], + 0, "Bez účelu sítě je tabulka jen sloupec čísel."), + ]), + L("Redundance", "Redundancy", + "Druhá cesta, která se opravdu použije.", + "A second path that is actually used.", + [ + S("1 / 3 • Dvojice", "Dva boxy nestačí", + "Tip: dva switche se stejnou konfigurací nejsou záloha, dokud provoz neumí přejít.", + "Redundance chce druhou cestu i protokol, který ji hlídá.", + "STP řeší smyčku na druhé vrstvě.", + "Na třetí vrstvě zálohu hlídá směrování nebo HSRP a příbuzné protokoly.", + "Jedna brána v DHCP znamená, že pád routeru odstřihne všechny."), + S("2 / 3 • Brána", "Virtuální adresa", + "Tip: stanice mají jako bránu virtuální adresu, ne jednu fyzickou krabici.", + "Dva směrovače sdílejí jednu adresu brány.", + "Aktivní drží provoz, druhý čeká.", + "Když aktivní ztichne, druhý adresu převezme.", + "Stanice nemusí dostat nové DHCP."), + S("3 / 3 • Zkouška", "Nevyzkoušená záloha je teorie", + "Tip: vytáhněte kabel v okně, kdy smíte, ne až při maturitní písemce.", + "Ověřte, že záložní cesta opravdu přenáší data.", + "Sledujte, za jak dlouho se provoz vrátí.", + "Záložní zdroj má jinou pojistku a jinou fázi, když to jde.", + "Zápis z zkoušky říká, co se má stát příště."), + ], + [ + Q("Proč dva stejné routery samy o sobě nestačí?", + ["Stanice musí mít kam přejít, když jeden ztichne", + "IPv4 povoluje jen jeden router v budově", + "Druhý router maže VLAN", + "STP dva routery zakazuje"], + 0, "Bez společné nebo přepínané brány počítače zůstanou na mrtvé adrese."), + Q("Co stanice používá při protokolu virtuální brány?", + ["Virtuální adresu, ne konkrétní krabici", + "Svou vlastní MAC jako bránu", + "Adresu DNS", + "Broadcast 255.255.255.255 jako další skok"], + 0, "Převzetí adresy stanice nepozná jako výměnu DHCP."), + Q("Kdy se záloha ověřuje?", + ["Nanečisto, dřív než o ni opřete provoz", + "Až když síť leží", + "Jen na papíře", + "Záloha se ověřovat nemá, ať se nepokazí"], + 0, "Nevyzkoušený přepínač překvapí v nejhorší chvíli."), + Q("Co na druhé vrstvě řeší náhradní kabel?", + ["STP, jinak vznikne smyčka", + "Jen NAT", + "Jen WPA3", + "Nic, ethernet smyčky umí sám"], + 0, "Druhý kabel bez STP je recept na bouři."), + ]), + L("802.1X", "802.1X", + "Port pustí data, až když se zařízení představí.", + "A port forwards data only after the device identifies itself.", + [ + S("1 / 3 • Port", "Nejdřív průkaz", + "Tip: dokud ověření neskončí, na portu nemá téct běžný provoz.", + "Počítač se prokáže certifikátem nebo jménem a heslem.", + "Switch se ptá serveru RADIUS.", + "Až pak pustí port do dané VLAN.", + "Neznámé zařízení zůstane venku nebo v omezené síti."), + S("2 / 3 • Lidé a věci", "Ne všechno umí 802.1X", + "Tip: tiskárna bez klienta potřebuje výjimku, ne vypnutí celé ochrany.", + "Počítač v doméně zvládne ověření sám.", + "Tiskárna, kamera a telefon často ne.", + "MAB pustí zařízení podle MAC adresy. Tu jde zfalšovat.", + "Výjimka má být na konkrétní port, ne na celý switch."), + S("3 / 3 • VLAN", "Kam koho pustit", + "Tip: učitel a host nemají po ověření skončit ve stejné VLAN.", + "Úspěch může přiřadit VLAN podle skupiny.", + "Neúspěch skončí v síti bez přístupu k serverům.", + "Účet jde odebrat, aniž by se měnilo heslo Wi-Fi.", + "Stejný princip sedí na kabel i na podnikovou Wi-Fi."), + ], + [ + Q("Kdo rozhodne, že zařízení smí do sítě?", + ["RADIUS na základě údajů od switche", + "Sám kabel podle barvy", + "DNS záznam A", + "STP root"], + 0, "Switch se ptá, server odpovídá."), + Q("Proč MAB není silné ověření?", + ["MAC adresu lze zfalšovat", + "MAC adresa je jednorázové heslo", + "MAB šifruje silněji než certifikát", + "Funguje jen na IPv6"], + 0, "Adresa na štítku není průkaz."), + Q("Kam patří zařízení, které ověřením neprojde?", + ["Do omezené sítě nebo nikam", + "Do VLAN serverů", + "Na trunk všech VLAN", + "Do správy routeru"], + 0, "Neúspěch nemá otevírat vnitřní síť."), + Q("Jaká je výhoda účtů proti jednomu heslu Wi-Fi?", + ["Jednomu člověku jde přístup odebrat", + "Heslo se nemusí nikdy měnit, protože účty ho ruší", + "Netřeba RADIUS ani switch", + "WEP pak stačí"], + 0, "Sdílené heslo po odchodu člověka zná pořád."), + ]), + L("Hledání chyby", "Troubleshooting", + "Postup zdola nahoru, ne náhodné restarty.", + "Bottom-up steps, not random reboots.", + [ + S("1 / 3 • Vrstvy", "Nejdřív světlo na portu", + "Tip: nehledejte chybu v DNS, když port nesvítí.", + "Fyzická vrstva: kabel, port, napájení, duplex, chyby.", + "Druhá vrstva: VLAN, MAC tabulka, trunk, STP.", + "Třetí vrstva: adresa, maska, brána, trasa, ACL.", + "Výš: DNS, port služby, firewall na konci."), + S("2 / 3 • Jedna změna", "Ať víte, co pomohlo", + "Tip: pět změn naráz nic nenaučí.", + "Zapište, co vidíte, než něco přepnete.", + "Ověřte jednu hypotézu.", + "Ping na bránu, pak za ni, pak jméno.", + "Když oprava sedí, uložte konfiguraci a dopište důvod."), + S("3 / 3 • Rozsah", "Koho to postihlo", + "Tip: jeden počítač je jiná chyba než celá učebna.", + "Jeden stroj: jeho kabel, port, adresa, účet.", + "Celá VLAN: brána, DHCP, trunk, SVI.", + "Celá škola: jádro, firewall, poskytovatel.", + "Čas začátku často sedí se změnou, kterou někdo neoznámil."), + ], + [ + Q("Co ověříte jako první, když port nesvítí?", + ["Kabel, port a napájení", "Záznam AAAA", + "Cenu OSPF", "Pravidlo QoS pro hlas"], + 0, "Vyšší vrstvy nemají na čem jet."), + Q("Proč měnit jen jednu věc?", + ["Ať je vidět, která změna pomohla", + "Switch víc změn zakazuje", + "IPv4 umí jen jednu opravu denně", + "DNS jinak smaže zónu"], + 0, "Více zásahů naráz zamaskuje příčinu."), + Q("Celá učebna nedostane adresu. Kde začnete?", + ["DHCP, VLAN a brána té sítě", + "Heslem jednoho studenta k webu", + "Jasem monitoru", + "Verzí prohlížeče"], + 0, "Společný příznak ukazuje na společnou službu."), + Q("Co uděláte, když oprava sedí?", + ["Uložíte konfiguraci a zapíšete důvod", + "Restartujete všechno ještě jednou", + "Smažete popis portů", + "Vypnete dohled, ať neruší"], + 0, "Neuložená oprava do rána zmizí."), + ]), + L("Cloud a hybrid", "Cloud and hybrid networks", + "Část služeb není ve skříni na chodbě.", + "Some services are no longer in the cupboard in the corridor.", + [ + S("1 / 3 • Kam s tím", "Vlastní sál, nebo cizí", + "Tip: cloud neruší síť ve škole, mění jen to, kam vede spoj.", + "Veřejný cloud provozuje poskytovatel a vy si berete službu.", + "Soukromý cloud je pořád vaše zařízení, jen obalené portálem.", + "Hybrid nechá část dat ve škole a část venku.", + "Účet a síťová cesta jsou pořád vaše odpovědnost."), + S("2 / 3 • Cesta", "Jak se tam školní síť dostane", + "Tip: služba v cloudu bez cesty a bez jmen je jen faktura.", + "VPN do cloudu vypadá jako další pobočka.", + "Veřejná služba jede přes internet a firewall.", + "DNS musí ukazovat na nové adresy.", + "Výpadek WAN teď shodí i to, co dřív jelo z místního serveru."), + S("3 / 3 • Hranice", "Kdo zálohuje a kdo pustí dovnitř", + "Tip: poskytovatel nezálohuje vaše data, dokud to není ve smlouvě.", + "Pravidla firewallu se stěhují s službou.", + "Účty v cloudu mají mít stejný řád jako účty ve škole.", + "Test obnovy platí i pro data, která nejsou ve vašem racku.", + "Škola pořád řeší osobní údaje, i když disk je jinde."), + ], + [ + Q("Co hybridní síť znamená?", + ["Část služeb je ve škole a část u poskytovatele", + "Dva switche v jedné VLAN", + "IPv4 i papírový sešit", + "Wi-Fi bez hesla"], + 0, "Hranice školy už není hranicí všech služeb."), + Q("Co se pokazí, když WAN spadne a pošta je jen v cloudu?", + ["Škola se k poště nedostane", + "Místní switch přestane dělat VLAN", + "STP zvolí cloud jako root", + "Nic, cloud WAN nepotřebuje"], + 0, "Cesta ven je součást dostupnosti služby."), + Q("Kdo zálohuje data v cloudu?", + ["Ten, koho to smlouva a nastavení opravdu určují", + "Vždy automaticky stát", + "STP", + "Nikdo, cloud zálohy zakazuje"], + 0, "Předpoklad, že poskytovatel zálohuje vše, často neplatí."), + Q("Co škola řeší i u cizího disku?", + ["Kdo má přístup a jak se chrání osobní údaje", + "Jen barvu loga", + "Jen typ ventilátoru v sále poskytovatele", + "Nic, odpovědnost přechází celá"], + 0, "Umístění dat nemaže pravidla přístupu."), + ]), + L("Automatizace", "Automation", + "Opakovanou práci ať dělá skript, ne deset kliknutí.", + "Let a script do the repeated work, not ten clicks.", + [ + S("1 / 3 • SSH", "Správa bez společného hesla na lístečku", + "Tip: Telnet posílá heslo čitelně. Na správu patří SSH.", + "Klíč jde odebrat konkrétnímu člověku.", + "Heslo v příkazu historie nemá ležet.", + "Přístup omezte na správní síť.", + "Záznam, kdo se přihlásil, patří do logu."), + S("2 / 3 • Opakování", "Stejná VLAN ve dvaceti switchích", + "Tip: ruční opis se v desátém switchi liší v jednom čísle.", + "Šablona drží názvy VLAN a popis portů.", + "Skript změnu pošle na seznam zařízení.", + "Nejdřív ho pusťte na jednom switchi.", + "Záloha před hromadnou změnou je povinnost, ne ozdoba."), + S("3 / 3 • Mez", "Skript není výmluva", + "Tip: automat neopraví špatný záměr, jen ho provede rychleji.", + "Kontrola po běhu srovná, co opravdu sedí.", + "Tajné heslo nepatří do veřejného souboru.", + "Člověk pořád schvaluje změnu, která shodí výuku.", + "Verzovaný soubor konfigurace ukáže, kdo řádek změnil."), + ], + [ + Q("Proč na správu nepoužívat Telnet?", + ["Heslo jde po síti čitelně", + "Telnet neumí IPv4", + "Switch Telnet zakazuje standardem STP", + "Telnet maže VLAN"], + 0, "SSH spojení chrání."), + Q("Jak pustíte stejnou změnu do dvaceti switchů?", + ["Šablonou a skriptem, neopisovat ji dvacetkrát", + "Jen restartem všech naráz", + "Přes DHCP", + "Jedním broadcastem bez kontroly"], + 0, "Nejdřív jeden switch, pak ostatní."), + Q("Co před hromadnou změnou uděláte?", + ["Zálohu konfigurací", + "Vypnete dohled", + "Smažete popisy portů", + "Zakážete SSH"], + 0, "Ať se máte kam vrátit."), + Q("Co skript neumí?", + ["Poznat, že záměr byl špatně", + "Poslat stejný příkaz víckrát", + "Uložit konfiguraci", + "Připojit se přes SSH"], + 0, "Rychlost neopraví chybný příkaz."), + ]), + L("Provoz a maturita", "Operations and the exam", + "Síť ve škole má pravidla, ne jen blikající porty.", + "A school network has rules, not only blinking ports.", + [ + S("1 / 3 • Řád", "Kdo smí co změnit", + "Tip: heslo admin na lístečku u switche není provozní řád.", + "Účty jsou jmenné, ne jedno společné admin.", + "Změna ve výuce se hlásí dopředu.", + "Hostovská síť je oddělená a časově omezená.", + "Osobní údaje v logu se nenechávají ležet bez důvodu."), + S("2 / 3 • Výpadek", "Co říct a co udělat", + "Tip: nejdřív rozsah a dopad, pak ladění detailu.", + "Řekněte, co nejede a koho se to týká.", + "Oddělte výpadek poskytovatele od chyby ve škole.", + "Po obnově ověřte poštu, přihlášení i tisk, ne jen ping.", + "Krátký zápis příště zkrátí stejnou poruchu."), + S("3 / 3 • U zkoušky", "Co umět ukázat", + "Tip: maturita chce důvod, ne jen zkratku.", + "Umíte přečíst adresu, masku, bránu a VLAN.", + "Umíte říct, k čemu je trunk, NAT, firewall a záloha.", + "Schéma malé sítě nakreslíte i bez nápovědy.", + "Postup hledání chyby od kabelu po službu odvykládáte po vrstvách."), + ], + [ + Q("Jak nemají vypadat účty ke switchi?", + ["Jedno heslo admin pro celou sborovnu", + "Jmenný účet s možností odebrání", + "Přístup jen ze správní sítě", + "Záznam přihlášení"], + 0, "Společné heslo nejde vzít jednomu člověku."), + Q("Co řeknete jako první při výpadku?", + ["Co nejede a koho se to týká", + "Celou směrovací tabulku zpaměti", + "Typ ventilátoru v racku", + "Že se to vyřeší samo"], + 0, "Rozsah je důležitější než první domněnka."), + Q("Co po obnově nestačí?", + ["Jen ping na bránu", + "Ověřit poštu a přihlášení", + "Podívat se na tisk", + "Zkontrolovat, že VLAN sedí"], + 0, "Brána může žít a služba pořád ne."), + Q("Co u maturity k VLAN umíte říct?", + ["Odděluje provoz a mezi VLAN se směruje", + "Je to typ pevného disku", + "Nahrazuje zálohu konfigurace", + "Je totéž co WPA3"], + 0, "Definice a důvod, ne jen zkratka."), + ]), + L("Dokumentace sítě", "Network documentation", + "Schéma, které sedí s tím, co je ve zdi.", + "A diagram that matches what is actually in the wall.", + [ + S("1 / 3 • Schéma", "Co na něm má být", + "Tip: schéma z doby instalace bez datumu je pohádka.", + "Jsou na něm jádro, učebny, VLAN a přípojka ven.", + "Adresní plán sedí se schématem, ne s pamětí.", + "Porty páteře mají popisky na obou koncích.", + "Datum a jméno autora říkají, jak je obrázek starý."), + S("2 / 3 • Změna", "Nejdřív papír, pak kabel", + "Tip: přepojit a zapsat až za měsíc znamená, že zápis už nesedí.", + "Změna VLAN se zapíše ve stejný den.", + "Fotka rozvaděče doplní schéma tam, kde čáry lžou.", + "Hesla do schématu nepatří. Patří do trezoru účtů.", + "Kopie schématu není jen na počítači, který ta síť živí."), + S("3 / 3 • Předání", "Až tu nebudete", + "Tip: kolega má síť převzít bez vašeho telefonu.", + "Seznam zařízení, adres správy a kde leží zálohy.", + "Kontakt na poskytovatele a číslo spoje.", + "Co se nesmí restartovat ve výuce.", + "Jedna stránka je lepší než nikde neuložená hlava."), + ], + [ + Q("Co do schématu sítě nepatří?", + ["Heslo správce", "VLAN a jejich účel", + "Přípojka k poskytovateli", "Datum, kdy naposledy sedělo"], + 0, "Heslo patří jinam než na obrázek, který visí ve sborovně."), + Q("Kdy se změna zapíše?", + ["V den, kdy se provede", "Až se na ni někdo zeptá u maturity", + "Jen když spadne celá škola", "Zápis není potřeba, stačí paměť"], + 0, "Nezapsaná změna je příští závada."), + Q("Proč mít kopii schématu i mimo školní server?", + ["Když síť neběží, schéma na ní neotevřete", + "Server schémata maže", + "IPv6 schémata zakazuje", + "STP obrázky nepouští"], + 0, "Dokumentace má přežít výpadek toho, co popisuje."), + Q("Co má kolega najít bez vašeho telefonu?", + ["Adresy správy, zálohy a kontakt na poskytovatele", + "Jen barvu racku", + "Vaše soukromé heslo", + "Nic, síť se nepředává"], + 0, "Provoz nesmí stát na jednom člověku."), + ]), + ], +) + +HW2 = course( + "hw2", + "Monitory, tisk, sběrnice, chlazení, sestavení, UEFI a diagnostika.", + "Displays, printing, buses, cooling, assembly, UEFI and diagnostics.", + [ + L("Monitory", "Displays", + "Panel, rozlišení a proč 60 Hz není jediné číslo.", + "The panel, the resolution, and why 60 Hz is not the only number.", + [ + S("1 / 3 • Panel", "LCD a OLED", + "Tip: rozbité podsvícení LCD zhasne obraz, i když panel pořád kreslí.", + "LCD svítí podsvícením a panel světlo cloní.", + "OLED svítí samotnými body a umí opravdovou černou.", + "Lesklý panel má vyšší kontrast a víc odlesků.", + "Uhlopříčka v palcích není totéž co pracovní plocha v bodech."), + S("2 / 3 • Obraz", "Rozlišení a frekvence", + "Tip: nativní rozlišení panelu je to, na které je mřížka bodů.", + "Nižší rozlišení interpoluje a obraz měkne.", + "Obnovovací frekvence říká, kolikrát za sekundu se obraz překreslí.", + "Odezva panelu není totéž co frekvence.", + "HDR chce vyšší jas, ne jen zaškrtnutou volbu."), + S("3 / 3 • Kabel", "Obraz musí stihnout pásmo", + "Tip: starý kabel nebo slabá verze HDMI oseká frekvenci i rozlišení.", + "DisplayPort a HDMI mají verze s různým datovým tokem.", + "VGA je analogové a na dlouhém kabelu měkne.", + "USB-C obraz poveze jen tehdy, když port umí alternativní režim.", + "Dokovací stanice má vlastní limit, ne limit notebooku."), + ], + [ + Q("Čím se OLED liší od běžného LCD?", + ["Body samy svítí a černá je opravdu zhasnutá", + "Nemá rozlišení", + "Jede jen po VGA", + "Nepotřebuje grafickou kartu"], + 0, "LCD obraz skládá cloněním podsvícení."), + Q("Co je nativní rozlišení?", + ["Počet bodů, na který je panel vyroben", + "Nejvyšší číslo v herním menu bez ohledu na panel", + "Rychlost procesoru", + "Počet USB portů"], + 0, "Jiné rozlišení se musí přepočítat."), + Q("Proč 4K při 144 Hz někdy nejde?", + ["Kabel nebo verze portu nestihne datový tok", + "OLED zakazuje frekvenci nad 60 Hz", + "IPv4 omezuje monitory", + "BIOS maže rozlišení"], + 0, "Pásmo portu je strop, ne přání v menu."), + Q("Kdy USB-C nepřenese obraz?", + ["Když port umí jen data a nabíjení", + "Když je kabel černý", + "Když má notebook SSD", + "Když je v síti VLAN"], + 0, "Konektor stejného tvaru ještě neznamená DisplayPort v něm."), + ]), + L("Tisk a sken", "Print and scan", + "Laser, inkoust a síťová fronta.", + "Laser, inkjet, and a network queue.", + [ + S("1 / 3 • Princip", "Toner a kapky", + "Tip: laser netaví toner na papír sám, dělá to zapékací válec.", + "Laser nabije válec a toner se chytí podle obrazu.", + "Inkoust stříká kapky a hodí se na fotky i občasný tisk.", + "Zaschnutá hlava je častější než porucha desky.", + "Termotransfer na štítky není totéž co kancelářský laser."), + S("2 / 3 • Síť", "Jedna tiskárna pro učebnu", + "Tip: tiskárna s vlastní adresou má rezervaci v DHCP.", + "Sdílená tiskárna na počítači žije, jen když je ten počítač zapnutý.", + "Síťová tiskárna poslouchá sama.", + "Fronta na serveru drží ovladač na jednom místě.", + "Špatný ovladač tiskne znaky místo stránky."), + S("3 / 3 • Sken", "Obraz zpět do počítače", + "Tip: rozlišení 600 dpi na text stačí, plakát chce víc.", + "Plochý skener i podavač mají jiný limit gramáže.", + "ADF nezvládne sešité listy.", + "Síťový sken může poslat soubor do složky nebo na poštu.", + "Sklo a podavač se mažou, jinak jsou na stránce pruhy."), + ], + [ + Q("Co v laserové tiskárně připevní toner k papíru?", + ["Zapékací válec", "Inkoustová hlava", + "Ventilátor zdroje", "VGA kabel"], + 0, "Teplo a tlak toner zataví."), + Q("Kdy sdílená tiskárna na učitelském počítači netiskne?", + ["Když je ten počítač vypnutý", + "Když má tiskárna vlastní IP a server", + "Když je v síti DHCP", + "Když je papír v zásobníku"], + 0, "Sdílení přes počítač z toho počítače dělá server."), + Q("Proč dát tiskárně rezervaci v DHCP?", + ["Aby ovladače mířily pořád na stejnou adresu", + "Aby se zvýšilo rozlišení", + "Aby toner vydržel dýl", + "Aby nešla skenovat"], + 0, "Nová adresa po restartu rozbije frontu."), + Q("Co podavač skeneru nezvládne?", + ["Sešitou sadu listů", "Jednotlivé běžné listy", + "Černobílý text", "Odeslání do složky, pokud to umí firmware"], + 0, "Sešit patří na sklo, ne do ADF."), + ]), + L("Sběrnice a disky", "Buses and drives", + "USB, SATA a NVMe nejsou zaměnitelné jen konektorem.", + "USB, SATA and NVMe are not interchangeable just because of the plug.", + [ + S("1 / 3 • USB", "Versie a napájení", + "Tip: port USB-A 2.0 fyzicky vezme i rychlý flash disk a stáhne ho na svou rychlost.", + "USB 2.0, 3.x a 4 se liší tokem dat.", + "USB-C je tvar, ne záruka rychlosti.", + "Thunderbolt po stejném tvaru umí obraz, síť i disk.", + "Hub bez napájení nestačí pro víc disků."), + S("2 / 3 • SATA", "Klasické disky", + "Tip: napájení SATA a datový kabel jsou dva různé vodiče.", + "HDD i starší SSD sedí na SATA.", + "Rychlost rozhraní je strop, disk může být pomalejší.", + "Kabel bez západky vypadne a disk zmizí z BIOSu.", + "Externí box schová SATA za USB."), + S("3 / 3 • NVMe", "SSD přímo na PCIe", + "Tip: M.2 je rozměr destičky. NVMe a SATA na M.2 nejsou totéž.", + "NVMe jede po PCIe a obchází starý řadič SATA.", + "Slot M.2 na desce může umět jen jednu z variant.", + "Dlouhá destička do krátkého slotu nepatří.", + "Bez chlazení rychlé SSD ve špičce zpomalí."), + ], + [ + Q("Co říká samotný tvar USB-C?", + ["Jen tvar konektoru, ne rychlost ani obraz", + "Vždy Thunderbolt", + "Vždy 40 Gb/s", + "Vždy nabíjení 100 W a DisplayPort"], + 0, "Schopnosti určuje řadič a kabel, ne obrys."), + Q("Čím se NVMe liší od SATA SSD?", + ["Jede po PCIe, ne po starém SATA", + "Nemá paměťové buňky", + "Jde zapojit jen do VGA", + "Nepotřebuje desku"], + 0, "Protokol a sběrnice jsou rychlejší cesta."), + Q("Co může umět slot M.2?", + ["Jen SATA, jen NVMe, nebo obojí, podle desky", + "Vždy obojí", + "Jen grafickou kartu", + "Jen operační paměť"], + 0, "Manuál desky je závaznější než tvar zářezu."), + Q("Proč rychlé SSD ve špičce zpomalí?", + ["Přehřeje se a stáhne výkon", + "SATA mu sebere VLAN", + "BIOS maže buňky", + "USB-C ho přepne na 2.0 vždy"], + 0, "Řadič hlídá teplotu."), + ]), + L("Chlazení", "Cooling", + "Vzduch musí mít kudy dovnitř a ven.", + "Air needs a way in and a way out.", + [ + S("1 / 3 • Tok", "Podtlak a přetlak", + "Tip: všechny ventilátory foukající dovnitř bez odtahu jen víří prach.", + "Vpředu a dole se vzduch nasává, vzadu a nahoře odchází.", + "Filtr patří na vstup, ne na výfuk.", + "Zavřená přední stěna designové skříně škrtí přívod.", + "Kabel přes ventilátor je ucpaný filtr."), + S("2 / 3 • CPU", "Chladič musí sednout", + "Tip: pasta není vrstva másla. Má vyplnit nerovnosti.", + "Vzduchový chladič potřebuje místo k bočnici.", + "Voda odvede teplo do radiátoru, který pořád potřebuje vzduch.", + "Špatně dotažený chladič pustí teplotu hned po startu.", + "Limit teploty v BIOSu je pojistka, ne pracovní režim."), + S("3 / 3 • Prach", "Tichý vrah zdroje", + "Tip: vrstva prachu na žebru je izolace.", + "Zdroj s filtrem se čistí, nezalepuje.", + "Notebookový výfuk do měkké deky se udusí.", + "Po roce bez čištění rostou otáčky a teplota.", + "Čištění stlačeným vzduchem nedrží ventilátor v roztočeném stavu naslepo."), + ], + [ + Q("Kam patří prachový filtr?", + ["Na vstup vzduchu", "Jen na výfuk vzadu", + "Dovnitř zdroje bez mřížky", "Na displej"], + 0, "Filtruje se to, co teprve vstupuje."), + Q("Co dělá příliš silná vrstva pasty?", + ["Zhorší přestup tepla", "Zvýší takt samo", + "Nahradí chladič", "Opraví špatný socket"], + 0, "Pasta vede hůř než kov chladiče."), + Q("Proč vodní chlazení pořád potřebuje vzduch?", + ["Radiátor musí teplo předat do místnosti", + "Voda chladí jen při otevřeném okně skříně", + "Čerpadlo jede na VGA", + "Bez ventilátoru se voda vaří vždy do minuty"], + 0, "Okruh teplo přesune, radiátor ho odvede."), + Q("Co udělá notebook na dece?", + ["Zakryje sání a přehřeje se", + "Zvýší jas panelu", + "Přepne disk na NVMe", + "Opraví ventilátor"], + 0, "Sání bývá zespodu."), + ]), + L("Sestavení", "Building a PC", + "Pořadí, které nerozbije desku.", + "An order that does not break the board.", + [ + S("1 / 3 • Příprava", "Nejdřív mimo skříň", + "Tip: sestavu nejdřív oživte na stole. Ve skříni se špatně hledá nedosedlá paměť.", + "Deska, procesor, chladič a jeden modul paměti.", + "Zdroj mimo skříň a obraz na základní grafiku.", + "Až potom disk, karty a zbytek paměti.", + "Antistatická podložka a klidné ruce porazí koberec."), + S("2 / 3 • Socket", "Procesor nemá kam spadnout", + "Tip: ohnutý pin na desce LGA nevrátíte pinzetou naslepo.", + "Trojúhelník na procesoru sedí se značkou na socketu.", + "Páka se zavírá až když procesor leží rovně.", + "Pasta až po usazení, ne do socketu.", + "Chladič se dotahuje křížem, ne jednou stranou nadoraz."), + S("3 / 3 • Skříň", "Kabely jsou součást airflow", + "Tip: distanční sloupky pod deskou musí sedět s otvory, jinak deska zkratuje.", + "Nejdřív zdroj a distanční sloupky, potom deska.", + "Napájení desky, procesoru a karty jsou tři různé konektory.", + "Přední USB a tlačítko mají malé konektory podle manuálu.", + "První start se sleduje: ventilátory, obraz, pípnutí nebo LED."), + ], + [ + Q("Proč oživit sestavu nejdřív mimo skříň?", + ["Nedosedlá paměť se ve skříni hledá hůř", + "Ve skříni nejde zapnout zdroj", + "BIOS ve skříni neběží", + "Pasta venku schne"], + 0, "Minimum dílů na stole ukáže, jestli jádro žije."), + Q("Co se nesmí stát pinům na socketu LGA?", + ["Ohnout je", "Nechat je v socketu", + "Srovnat procesor podle značky", + "Zavřít páku až po usazení"], + 0, "Pin na desce je křehký."), + Q("K čemu jsou distanční sloupky?", + ["Drží desku dál od plechu skříně", + "Napájí ventilátor", + "Jsou to šrouby do disku", + "Uzemňují jen přední LED"], + 0, "Bez nich nebo se špatnými sloupky deska zkratuje."), + Q("Který konektor nepatří do napájení procesoru?", + ["PCIe konektor grafické karty místo EPS", + "Hlavní 24pin na desku", + "EPS konektor k socketu", + "SATA napájení disku"], + 0, "Konektory si jsou podobné a sedí špatně záměrně."), + ]), + L("UEFI a boot", "UEFI and boot", + "Co se děje, než naběhne systém.", + "What happens before the operating system starts.", + [ + S("1 / 3 • Firmware", "BIOS a UEFI", + "Tip: UEFI není jen barevné menu. Umí jiné dělení disku a Secure Boot.", + "Firmware otestuje železo a najde disk se systémem.", + "UEFI používá tabulku GPT, starý BIOS často MBR.", + "Secure Boot pustí jen podepsaný zavaděč.", + "Vypnutí Secure Bootu je výjimka, ne výchozí rada."), + S("2 / 3 • Pořadí", "Z čeho se startuje", + "Tip: flash disk v pořadí před systémem nabootuje omylem instalaci.", + "Nabídka bootu umí jednorázově vybrat zařízení.", + "Disk bez zavaděče se v seznamu nechytí.", + "Více systémů potřebuje bootovací nabídku, ne přepisování pořád dokola.", + "Síťový boot PXE hledá obraz na serveru."), + S("3 / 3 • Heslo", "Menu není pro celou třídu", + "Tip: heslo BIOSu bez záznamu je past, až se bude měnit disk.", + "Heslo na setup brání přehazování bootu.", + "Heslo na disk je silnější a bez něj jsou data nedostupná.", + "Clear CMOS heslo setupu často smaže a vezme i nastavení.", + "Po změně profilu paměti ověřte, že systém naběhne dvakrát."), + ], + [ + Q("Co Secure Boot hlídá?", + ["Aby se spustil jen důvěryhodný zavaděč", + "Aby měl disk aspoň 1 TB", + "Aby ventilátor točil na maximum", + "Aby nešel USB flash disk vůbec zapojit"], + 0, "Nepodepsaný zavaděč se nespustí."), + Q("Čím se liší GPT od MBR?", + ["GPT je tabulka oddílů pro UEFI a větší disky", + "GPT je typ pasty", + "MBR je jen pro NVMe", + "Jsou to totéž"], + 0, "Starý MBR má omezení, na která velký disk naráží."), + Q("Proč nenechat flash disk trvale první v bootu?", + ["Omylem z něj naběhne cizí systém", + "Flash disk vypne Secure Boot navždy", + "UEFI flash disky zakazuje", + "Smaže TPM"], + 0, "Jednorázová nabídka bootu je bezpečnější."), + Q("Co často udělá vymazání CMOS?", + ["Smaže heslo setupu i ostatní volby", + "Smaže jen soubory na ploše", + "Přepne Windows na jiný jazyk", + "Opraví ohnutý pin"], + 0, "Je to návrat firmware k výchozímu stavu."), + ]), + L("Diagnostika", "Diagnostics", + "POST, pípání a minimum, které ještě naběhne.", + "POST, beep codes, and the smallest build that still starts.", + [ + S("1 / 3 • POST", "Test hned po zapnutí", + "Tip: žádný obraz a žádné pípnutí neznamená automaticky mrtvý procesor.", + "Firmware zkouší procesor, paměť a grafiku.", + "Kódy na displeji desky jsou přesnější než staré pípání.", + "Reproduktor skříně musí být zapojený, jinak ticho nic neřekne.", + "Manuál desky překládá kódy. Nejsou u všech stejné."), + S("2 / 3 • Minimum", "Uberte, co může lhát", + "Tip: jeden modul paměti v doporučeném slotu porazí čtyři najednou.", + "Ven grafika, pokud má procesor vlastní obraz.", + "Ven disky a karty.", + "Zkuste jiný kabel obrazu a jiný port.", + "Až jádro naběhne, vracejte díly po jednom."), + S("3 / 3 • Paměť a disk", "Náhodné pády", + "Tip: pád až ve Windows může být paměť, ne jen ovladač.", + "Test paměti běží déle než jedno pípnutí.", + "Chyby čtení disku se projeví v jeho vlastních statistikách.", + "Přehřátí shodí stroj až při zátěži, ne v menu BIOSu.", + "Vyměněný díl ověřte stejnou zkouškou, která předtím selhala."), + ], + [ + Q("Co POST dělá?", + ["Po zapnutí zkouší základní díly", + "Instaluje systém", + "Formátuje SSD", + "Stahuje ovladače z internetu"], + 0, "Je to první test firmware, ne operační systém."), + Q("Proč začít s jedním modulem paměti?", + ["Špatný modul nebo slot se snáz odhalí", + "Deska víc modulů zakazuje", + "BIOS umí jen 4 GB", + "Druhý modul maže UEFI"], + 0, "Minimum dílů zužuje chybu."), + Q("Kdy se přehřátí často neprojeví?", + ["V menu BIOSu bez zátěže", + "Při dlouhém testu procesoru", + "Ve hře", + "Při kódování videa"], + 0, "Teplota roste až s odběrem."), + Q("Co ověří výměnu dílu?", + ["Stejná zkouška, která předtím selhala", + "Jen to, že ventilátor se točí", + "Nová tapeta", + "Jiná barva skříně"], + 0, "Bez opakování testu nevíte, jestli to byla příčina."), + ]), + L("Porty a dokování", "Ports and docks", + "Co z notebooku opravdu vyleze.", + "What a laptop can actually drive.", + [ + S("1 / 3 • Dok", "Jeden kabel, mnoho omezení", + "Tip: levný USB hub není dok. Nepřenese obraz, který port neumí.", + "Dok může nést obraz, síť i napájení.", + "Limit je nejslabší článek: port, kabel nebo dok.", + "Dva externí monitory chtějí port, který to umí, ne přání uživatele.", + "Napájení přes dok nemusí stačit hernímu notebooku."), + S("2 / 3 • Síť", "Kabel z notebooku", + "Tip: mnoho tenkých notebooků už ethernet nemá a potřebuje adaptér.", + "USB adaptér je síťová karta mimo šasi.", + "Rychlost adaptéru bývá 1 Gb/s, ne rychlost školní páteře.", + "Dok s vlastní síťovkou má vlastní MAC adresu.", + "Rezervace DHCP na MAC notebooku pak neplatí pro dok."), + S("3 / 3 • Výběr", "Na co se ptát", + "Tip: stejný USB-C na dvou noteboocích nemá stejné schopnosti.", + "Kolik obrazů, jaké rozlišení a jestli se notebook nabíjí.", + "Jestli síť jede z doku, nebo se čeká Wi-Fi.", + "Jestli čtečka karet a USB sedí verzi, kterou škola používá.", + "Manuál portu je důležitější než fotka konektoru."), + ], + [ + Q("Kdy USB-C dok nepřenese dva monitory?", + ["Když port notebooku umí jen data", + "Když je dok černý", + "Když je v učebně VLAN", + "Když má disk NVMe"], + 0, "Dok neumí vyrobit schopnost, kterou port nemá."), + Q("Proč DHCP rezervace na notebook neplatí přes dok?", + ["Dok má vlastní MAC adresu", + "DHCP doky zakazuje", + "VLAN maže rezervace", + "Ethernet nemá MAC"], + 0, "Rezervace sedí na adresu, kterou server vidí."), + Q("Co je nejslabší článek řetězu obrazu?", + ["Port, kabel nebo dok, podle toho, co umí nejmíň", + "Vždy monitor", + "Vždy procesor", + "Vždy tapeta"], + 0, "Datový tok musí projít všemi."), + Q("Čím se liší USB hub od doku s obrazem?", + ["Hub sám o sobě obraz z datového portu nevyrobí", + "Hub je vždy rychlejší", + "Dok neumí napájení", + "Jsou to totéž"], + 0, "Obraz chce alternativní režim nebo DisplayPort v kabelu."), + ]), + ], +) + +HW3 = course( + "hw3", + "RAID, NAS, servery, virtualizace, zálohy, UPS a firmware.", + "RAID, NAS, servers, virtualisation, backups, UPS and firmware.", + [ + L("RAID", "RAID", + "Více disků jako jeden svazek, ne jako záloha.", + "Several drives as one volume, not as a backup.", + [ + S("1 / 3 • Zrcadlo", "RAID 1", + "Tip: zrcadlo přežije pád jednoho disku, ne smazání souboru.", + "Stejná data jsou na dvou discích.", + "Kapacita je jako jeden disk.", + "Čtení může být rychlejší, zápis ne dvojnásobný.", + "Po výměně disku se pole musí obnovit."), + S("2 / 3 • Prokládání", "RAID 0 a 5", + "Tip: RAID 0 při pádu jednoho disku ztratí všechno.", + "RAID 0 skládá rychlost a kapacitu a nemá ochranu.", + "RAID 5 snese pád jednoho disku z aspoň tří.", + "Parita zabere kapacitu jednoho disku.", + "Obnova velkého pole trvá dlouho a druhý pád v té době je zkáza."), + S("3 / 3 • Řadič", "Hardware a software", + "Tip: pole z jiného řadiče v novém serveru nemusí naběhnout.", + "Hardwarový řadič má vlastní cache a baterii.", + "Softwarové pole řeší systém a je přenosnější.", + "Cache bez ochrany při výpadku proudu rozbije zápis.", + "RAID nenahrazuje zálohu na jiné místo."), + ], + [ + Q("Co RAID 1 přežije?", + ["Pád jednoho z dvojice disků", + "Smazání souboru uživatelem", + "Požár místnosti", + "Ransomware na obou discích naráz"], + 0, "Oba disky mají stejná data, včetně chyby, kterou tam zapíšete."), + Q("Co je RAID 0?", + ["Prokládání bez ochrany při pádu disku", + "Zrcadlo", + "Záloha na pásku", + "Šifrování disku"], + 0, "Rychlost a kapacita, nulová odolnost."), + Q("Proč RAID není záloha?", + ["Neochrání před smazáním, malwarem ani požárem skříně", + "Protože neumí víc než dva disky", + "Protože Windows RAID zakazuje", + "Protože parita maže soubory"], + 0, "Záloha je další kopie jinde a v jiném čase."), + Q("Co hrozí velkému poli při obnově?", + ["Druhý disk spadne dřív, než se obnova dopočítá", + "Pole se samo přepne na RAID 0", + "BIOS smaže paritu", + "NIC se při obnově nesmí točit"], + 0, "Obnova je dlouhá a disky jsou stejně staré."), + ]), + L("NAS a sítě disků", "NAS and storage networks", + "Disky, které nejsou uvnitř jednoho počítače.", + "Drives that are not inside one computer.", + [ + S("1 / 3 • NAS", "Souborový server v krabici", + "Tip: NAS v učebně bez zálohy je jen jeden košík na všechna data.", + "Nabízí složky po síti.", + "Má vlastní účty nebo se ptá školního adresáře.", + "Dva porty se dají spojit, ale jen když to umí i switch.", + "Hlučný diskový provoz nepatří do tiché učebny."), + S("2 / 3 • SAN", "Disk jako místní", + "Tip: SAN nedává složku, dává blokové zařízení.", + "Server vidí disk, i když leží jinde.", + "Síť pro SAN se nemíchá s provozem žáků.", + "Špatné zónování vystaví disk cizímu serveru.", + "Výpadek této sítě shodí systémy, které na disku jedou."), + S("3 / 3 • Soubor", "Práva a snímky", + "Tip: snímek disku není záloha, dokud leží na stejném poli.", + "Práva na složce mají sedět se skupinami, ne s jedním účtem učitele.", + "Snímek umí vrátit soubor smazaný dopoledne.", + "Plný svazek zastaví i zdánlivě nesouvisející službu.", + "Kapacita se hlídá dřív, než svítí červeně."), + ], + [ + Q("Co NAS obvykle nabízí?", + ["Složky po síti", "Blokový disk celé škole bez oprávnění", + "Náhradu procesoru v noteboocích", "Obraz monitoru"], + 0, "Je to souborová služba."), + Q("Čím se SAN liší?", + ["Server dostane disk jako bloky, ne jako složku", + "Je to jen jiné jméno pro USB flash disk", + "Jede jen po Wi-Fi", + "Nemá disky"], + 0, "Souborový systém si na těch blocích dělá až server."), + Q("Proč snímek na stejném poli není záloha?", + ["Pád pole vezme i snímek", + "Snímek nejde vytvořit", + "Snímek maže RAID", + "SMB snímky zakazuje"], + 0, "Záloha musí přežít ztrátu původního úložiště."), + Q("Kam nepatří hlučné diskové pole?", + ["Do tiché učebny vedle žáků", + "Do racku s odtahem", + "Do místnosti se zámek", + "Do sítě oddělené od hostů"], + 0, "Hluk a přístup jsou součást návrhu."), + ]), + L("Servery a rack", "Servers and racks", + "Skříň, do které se nedává herní počítač naležato.", + "A cabinet that is not a gaming PC on its side.", + [ + S("1 / 3 • Tvar", "Racková jednotka", + "Tip: 1U je výška, ne výkon.", + "Server v racku se měří v jednotkách U.", + "Kolejnice musí unést hloubku skříně.", + "Napájecí lišta není prodlužovačka z kuchyně.", + "Proud a teplo se počítají dopředu, ne až když jistič spadne."), + S("2 / 3 • Vzduch", "Studená ulička", + "Tip: servery v racku sají vepředu a foukají dozadu. Otočený kus kazí ostatním.", + "Studený vzduch jde před servery, teplý za ně.", + "Zaslepení prázdných pozic nutí vzduch přes stroje.", + "Kabelová armáda před sáním je ucpávka.", + "Teplota v místnosti není teplota uvnitř 1U serveru."), + S("3 / 3 • Vzdáleně", "Když nejste ve škole", + "Tip: vzdálená správa má vlastní port a vlastní heslo.", + "IPMI nebo iDRAC žije i když systém ne.", + "Patří do správní sítě, ne do učebny.", + "Konzole v racku je záloha, když síť správy spadne.", + "Štítek na serveru říká, co na něm jede."), + ], + [ + Q("Co znamená 1U?", + ["Výšku zařízení v racku", "Výkon jednoho jádra", + "Rychlost disku", "Počet zdrojů"], + 0, "Je to rozměr, ne třída výkonu."), + Q("Kudy má server v racku sát vzduch?", + ["Vepředu a foukat dozadu", "Zezadu dopředu", + "Jen shora bez mřížky", "Z podlahy dírami v desce"], + 0, "Otočený server ohřívá sousedům sání."), + Q("K čemu je vzdálená správa serveru?", + ["Ovládáte ho, i když systém neběží", + "Nahrazuje zálohu dat", + "Je to Wi-Fi pro žáky", + "Maže RAID"], + 0, "Vidíte obraz a umíte stroj zapnout po síti."), + Q("Kam nepatří port vzdálené správy?", + ["Do žákovské VLAN", "Do správní sítě", + "Za firewall", "Na heslo jiné než admin na lístečku"], + 0, "Je to klíče od serveru."), + ]), + L("Virtualizace", "Virtualisation", + "Více systémů na jednom železe.", + "Several systems on one piece of hardware.", + [ + S("1 / 3 • Hypervizor", "Kdo rozděluje železo", + "Tip: virtuální počítač nemá vlastní zdroj. Dělí se o ten fyzický.", + "Hypervizor pouští více systémů nad jedním hardwarem.", + "Každý systém má vlastní diskový soubor a virtuální síťovku.", + "Paměť, kterou slíbíte navíc, se ve špičce potká.", + "Pád fyzického serveru shodí všechny hosty naráz."), + S("2 / 3 • Síť", "Virtuální switch", + "Tip: VLAN ve virtuálu musí sedět s trunkem na fyzické kartě.", + "Virtuální switch propojí hosty mezi sebou.", + "Most je pustí ven fyzickým portem.", + "Izolovaná síť jen mezi virtuály ven nevede.", + "Špatný trunk vystaví správní virtuál do učebny."), + S("3 / 3 • Disk", "Soubor není kouzlo", + "Tip: tenký disk roste a může zaplnit svazek, na kterém leží.", + "Tenké provizionování slíbí víc, než je na začátku obsazeno.", + "Snímek před změnou umí vrátit systém.", + "Zapomenuté snímky zpomalí disk a zaplní úložiště.", + "Záloha virtuálu patří mimo ten samý disk."), + ], + [ + Q("Co se stane, když spadne fyzický server s pěti virtuály?", + ["Spadnou všechny, pokud nemají kam utéct", + "Přežije aspoň správní virtuál", + "Hypervizor je přenese na notebook", + "Nic, virtuál zdroj nepotřebuje"], + 0, "Sdílejí jedno železo."), + Q("K čemu je most virtuálního switche?", + ["Pustí virtuál do fyzické sítě", + "Smaže VLAN", + "Nahradí RAID", + "Zapne Secure Boot hostitele"], + 0, "Bez mostu virtuál ven neodejde."), + Q("Čím je tenký disk nebezpečný?", + ["Doroste a zaplní úložiště pod sebou", + "Nejde zálohovat", + "Nemá souborový systém", + "Vypne hypervizor"], + 0, "Slíbená kapacita není volné místo."), + Q("Co udělá zapomenutý snímek?", + ["Roste a zpomaluje disk", + "Smaže heslo BIOSu", + "Opraví RAID 0", + "Přidá paměť"], + 0, "Snímek je dočasná pojistka, ne trvalý režim."), + ]), + L("Zálohování", "Backups", + "Kopie, kterou umíte vrátit.", + "A copy you can actually restore.", + [ + S("1 / 3 • Pravidlo", "Tři místa", + "Tip: kopie na tom samém disku přežije jen omyl v koši, ne pád disku.", + "Jedna kopie je málo, když leží u originálu.", + "Druhá má být mimo stroj a třetí mimo budovu, když na tom záleží.", + "Verze v čase vrátí soubor z pátku, ne jen ze včerejška.", + "Šifrovaná záloha bez klíče je hromada nic."), + S("2 / 3 • Co zálohovat", "Ne jen plochu", + "Tip: obraz systému bez dat školy je pěkná, ale prázdná záchrana.", + "Data, účty a konfigurace sítě jsou různé věci.", + "Poštovní schránka v cloudu má vlastní nastavení retence.", + "Databáze se nezálohuje kopírováním otevřeného souboru naslepo.", + "Seznam, co se nezálohuje, má být vědomý."), + S("3 / 3 • Zkouška", "Obnova je součást zálohy", + "Tip: záloha, kterou jste nikdy neobnovili, je hypotéza.", + "Jednou za čas obnovte soubor i celý systém nanečisto.", + "Měřte, jak dlouho obnova trvá.", + "Zápis říká, kdo má klíče a kde leží média.", + "Po ransomware rozhoduje stáří poslední čisté kopie."), + ], + [ + Q("Proč nestačí kopie na stejném disku?", + ["Pád disku vezme originál i kopii", + "Systém druhou složku zakazuje", + "RAID ji sám smaže", + "NTFS umí jen jeden soubor"], + 0, "Chyba média je společná."), + Q("Co není pořádná zkouška zálohy?", + ["Jen to, že noční úloha svítí zeleně", + "Obnova jednoho souboru", + "Změřený čas obnovy systému", + "Ověření, že klíč k šifře existuje"], + 0, "Zelená fajfka neznamená, že data jdou otevřít."), + Q("Co rozhoduje po ransomware?", + ["Jestli existuje čistá kopie starší než nákaza", + "Barva skříně", + "Počet ventilátorů", + "Verze spořiče obrazovky"], + 0, "Šifrovaná záloha z doby, kdy už malware běžel, je taky šifrovaná."), + Q("Proč nekopírovat otevřenou databázi jako obyčejný soubor?", + ["Rozpracovaný zápis bude nekonzistentní", + "Databáze nemá soubor", + "SMB databáze zakazuje", + "Kopie by smazala RAID"], + 0, "Běžící databáze se zálohuje nástrojem, který umí konzistentní snímek."), + ]), + L("UPS", "UPS", + "Minuty na uložení, ne náhradní elektrárna.", + "Minutes to shut down cleanly, not a spare power station.", + [ + S("1 / 3 • Úkol", "Překlenout výpadek", + "Tip: malá UPS neutáhne učebnu. Utáhne server, než se uloží.", + "Při výpadku přejde na baterii.", + "Čas běhu závisí na odběru, ne na nálepce bez zátěže.", + "Server dostane signál a vypne se po pořádku.", + "Tiskárna a monitor na UPS servery okrádají o minuty."), + S("2 / 3 • Tvar", "Ne každá UPS je pro zdroj serveru", + "Tip: levná UPS s obdélníkovým průběhem modernímu zdroji vadí.", + "Sinusový výstup je jistější pro aktivní PFC zdroje.", + "Výkon ve voltampérech není totéž co watty na štítku zdroje.", + "Přetížená UPS spadne právě ve výpadku.", + "Zkouška pod zátěží ukáže skutečné minuty."), + S("3 / 3 • Baterie", "Spotřební díl", + "Tip: baterie po třech letech v teple už slíbené minuty nedá.", + "Test baterie nečeká na první bouřku.", + "Výměna se dělá dřív, než článek nabobtná.", + "Teplá místnost baterii ničí rychleji.", + "UPS není náhrada za zálohu dat."), + ], + [ + Q("K čemu malá UPS u serveru je?", + ["Aby se systém stihl uložit a vypnout", + "Aby učebna jela celé odpoledne", + "Aby nahradila zálohu disků", + "Aby chladila rack"], + 0, "Jsou to minuty, ne směna."), + Q("Proč na UPS nepatří laserová tiskárna?", + ["Má velký odběr a sebere čas serverům", + "Tiskárna UPS zničí vždy okamžitě", + "USB tiskárny UPS zakazuje", + "Toner potřebuje jinou frekvenci"], + 0, "Špička zapékání baterii vybíjí zbytečně."), + Q("Co ukáže jen nálepka bez zátěže?", + ["Optimistický čas, který v provozu neplatí", + "Přesné minuty při plném serveru", + "Stav RAID", + "Teplotu procesoru"], + 0, "Měří se s tím, co na UPS opravdu visí."), + Q("Jak často baterie přestane držet slib?", + ["Po letech, dřív v teple", + "Nikdy, je součástí zdroje navždy", + "Až po deseti minutách prvního provozu", + "Jen když je UPS černá"], + 0, "Je to spotřební díl a má se zkoušet."), + ]), + L("Firmware", "Firmware", + "Aktualizace, která nejde udělat uprostřed hodiny.", + "An update that is not applied in the middle of a lesson.", + [ + S("1 / 3 • Co to je", "Program v železe", + "Tip: firmware není ovladač ve Windows. Žije v zařízení i bez systému.", + "Deska, disk, switch i BIOS mají vlastní verzi.", + "Chyba ve firmware shodí zařízení bez ohledu na systém.", + "Výrobce vydává opravy i roky po prodeji.", + "Neznámá verze se nejdřív zjistí, potom se teprve stahuje."), + S("2 / 3 • Postup", "Jedno zařízení, zdroj, záloha", + "Tip: aktualizace při bouřce bez UPS je způsob, jak dostat cihlu.", + "Přečtěte, co verze opravuje a co rozbije.", + "Záloha konfigurace switche jde bokem.", + "Napájení nesmí vypadnout v půlce zápisu.", + "Po dokončení ověřte, že naběhla i služba, ne jen dioda."), + S("3 / 3 • Řada", "Ne všechno v pátek odpoledne", + "Tip: neaktualizujte jádro sítě a jediný server ve stejné hodině.", + "Nejdřív méně důležité zařízení.", + "Okno, kdy smí síť ležet, je domluvené.", + "Když nová verze zlobí, musíte umět předchozí.", + "Zápis verze patří k zařízení, ne do hlavy jednoho člověka."), + ], + [ + Q("Čím se firmware liší od ovladače?", + ["Je v zařízení a běží i bez operačního systému", + "Je to jen soubor na ploše", + "Maže se s přeinstalací vždy", + "Umí ho jen tiskárna"], + 0, "BIOS a firmware disku přežijí nový systém."), + Q("Co před aktualizací switche uložíte?", + ["Konfiguraci a zajistíte napájení", + "Jen tapetu na ploše", + "Heslo Wi-Fi na tabuli", + "Nic, aktualizace si pamatuje sama"], + 0, "Přerušený zápis a chybějící záloha jsou dvě pasti."), + Q("Proč neaktualizovat jádro a jediný server naráz?", + ["Když obojí selže, nemáte z čeho učit ani spravovat", + "Výrobce to technicky zakazuje vždy", + "Firmware umí jen jedno zařízení na budovu", + "UPS to nesežere"], + 0, "Nechte si cestu zpět."), + Q("Co po aktualizaci ověříte?", + ["Že služba opravdu naběhla, ne jen že svítí dioda", + "Že je krabice teplejší", + "Že se změnila barva menu", + "Nic, firmware se neověřuje"], + 0, "Start zařízení není totéž co fungující síť."), + ]), + L("Měření výkonu", "Measuring performance", + "Číslo bez podmínek nic neznamená.", + "A number without the conditions means nothing.", + [ + S("1 / 3 • Co měřit", "Úzké hrdlo", + "Tip: rychlý procesor nezachrání pomalý disk, na kterém systém čeká.", + "Sledujte procesor, paměť, disk a síť zvlášť.", + "Vytížení 100 % chvíli není chyba, trvale ano.", + "Fronta na disku řekne víc než otáčky ventilátoru.", + "Jedno číslo z herního testu není návrh školního serveru."), + S("2 / 3 • Podmínky", "Stejná zkouška", + "Tip: srovnávejte stejný test, stejná data a stejný čas běhu.", + "Studený start a druhý běh z cache nejsou totéž.", + "Notebook na baterii schválně zpomalí.", + "Antivirus v testu umí sežrat výsledek.", + "Zapište verzi systému i to, co na pozadí jelo."), + S("3 / 3 • Rozhodnutí", "Kdy dokoupit", + "Tip: nejdřív paměť nebo disk, ne automaticky nejnovější procesor.", + "Málo paměti žene systém na disk a všechno zpomalí.", + "Plný a pomalý disk kazí i silný procesor.", + "Slabé Wi-Fi nezrychlíte výměnou grafické karty.", + "Nákup bez měření je odhad."), + ], + [ + Q("Co neznamená krátká špička procesoru na 100 %?", + ["Že je procesor nutně slabý", + "Že je všechno v pořádku navždy", + "Že disk neexistuje", + "Že se má hned vyměnit deska"], + 0, "Záleží, jestli špička trvá a jestli na ní někdo čeká."), + Q("Proč druhý běh testu bývá rychlejší?", + ["Data už jsou v cache", + "Procesor se sám přetaktuje natrvalo", + "Disk se zmenší", + "BIOS test podruhé vynechá"], + 0, "Cache není výkon železa v prvním čtení."), + Q("Co často zpomalí celý systém dřív než procesor?", + ["Málo paměti a pomalý disk", + "Příliš mnoho USB myš", + "Barva skříně", + "Počet ikon na ploše"], + 0, "Systém pak pořád čeká na stránkování."), + Q("Co k výsledku testu patří zapsat?", + ["Verze, napájení a co jelo na pozadí", + "Jen nejvyšší číslo", + "Jméno učitele", + "Heslo k BIOSu"], + 0, "Bez podmínek číslo nejde zopakovat."), + ]), + ], +) + +HW4 = course( + "hw4", + "Údržba, TPM, nákup, mobily, periferie, ekologie a maturita.", + "Maintenance, TPM, buying, mobiles, peripherals, disposal and the exam.", + [ + L("Údržba", "Maintenance", + "Čištění a revize dřív, než něco shoří.", + "Cleaning and checks before something burns.", + [ + S("1 / 3 • Prašnost", "Řád, ne až když hučí", + "Tip: otevřená skříň vysátá naslepo umí nasát prach dovnitř zdroje.", + "Filtry se perou dřív, než zčernají.", + "Zdroj se neotevírá. Čistí se zvenku.", + "Klávesnice a myš učebny jsou spotřební a špinavé.", + "Zápis, kdy se učebna dělala, příště určí interval."), + S("2 / 3 • Kontakty", "Uvolněný kabel vypadá jako mrtvá deska", + "Tip: než objednáte desku, domáčkněte napájení a vyzkoušejte jiný kabel obrazu.", + "Oxidy na stolních konektorech kazí obraz i USB.", + "Ohnutý konektor HDMI se do portu necpe silou.", + "Napájecí kabel s poškozenou izolací se vyřadí, nelepí páskou.", + "Prodlužovačka pod kobercem je požár, ne úspora místa."), + S("3 / 3 • Evidence", "Co ve třídě vůbec je", + "Tip: bez inventárního čísla nepoznáte, který zdroj se vrací do záruky.", + "Štítek, umístění a rok pořízení.", + "Záruka a doklad nejsou v šuplíku jednoho člověka.", + "Odepsaný stroj se nevrací do sítě s původním účtem.", + "Pravidelná prohlídka je levnější než hodina bez učebny."), + ], + [ + Q("Co se při údržbě se zdrojem nedělá?", + ["Neotevírá se", "Omývá se mu mřížka zvenku", + "Kontroluje se kabel", "Píše se, kdy byl čištěný"], + 0, "Uvnitř je napětí i po odpojení."), + Q("Co uděláte s kabelem s porušenou izolací?", + ["Vyřadíte ho", "Zalepíte izolepou a vrátíte pod koberec", + "Schováte ho za skříň", "Dáte ho k žákovskému počítači"], + 0, "Poškozená izolace je požár a úraz."), + Q("K čemu je inventární číslo?", + ["Aby šlo dohledat záruku a umístění", + "Aby zrychlilo procesor", + "Nahrazuje heslo BIOSu", + "Je to sériové číslo Windows"], + 0, "Bez něj je reklamace hádanka."), + Q("Kam nepatří odepsaný počítač?", + ["Zpět do sítě s původními účty", + "Do evidence odpadu", + "K smazání disků", + "Mimo žákovskou VLAN"], + 0, "Starý účet na mrtvém stroji je pořád díra, když ho někdo zapne."), + ]), + L("TPM a Secure Boot", "TPM and Secure Boot", + "Železo, které hlídá start systému.", + "Hardware that watches the system start.", + [ + S("1 / 3 • TPM", "Klíče mimo disk", + "Tip: TPM není heslo, které uživatel píše při každém startu.", + "Čip drží klíče použité při startu a šifrování disku.", + "Bez něj některé systémy šifrování disku odmítnou.", + "Vymazání TPM bez zálohy klíčů znamená ztrátu přístupu k disku.", + "Na virtuálu bývá TPM také virtuální a patří k tomu stroji."), + S("2 / 3 • Start", "Řetěz důvěry", + "Tip: Secure Boot a TPM řeší start, ne to, co uživatel spustí odpoledne.", + "Firmware ověří zavaděč.", + "Zavaděč ověří jádro.", + "Cizí systém z flash disku bez podpisu nenaběhne.", + "V laboratoři se výjimka zapíše, nenechá vypnutá navždy."), + S("3 / 3 • Zámek", "Kensington není šifra", + "Tip: lankový zámek zdrží odnesení, data chrání šifrování disku.", + "Zámek skříně zpomalí toho, kdo chce odnést RAM.", + "Heslo BIOSu zpomalí toho, kdo chce přeházet boot.", + "Šifra disku zastaví toho, kdo disk odnese.", + "Všechny tři vrstvy řeší jiného zloděje."), + ], + [ + Q("Co TPM drží?", + ["Klíče pro start a šifrování", + "Zálohu všech dokumentů školy", + "Obraz monitoru", + "Seznam VLAN"], + 0, "Není to úložiště souborů."), + Q("Co se stane po vymazání TPM bez zálohy klíčů?", + ["Šifrovaný disk nemusí jít otevřít", + "Zrychlí se start", + "Smaže se jen spořič", + "Nic, TPM klíče nepoužívá"], + 0, "Klíč v čipu bez kopie je pryč."), + Q("Co Secure Boot nezastaví?", + ["Program, který uživatel spustí v už naběhlém systému", + "Nepodepsaný zavaděč", + "Cizí systém z flashe, pokud podpis chybí", + "Změnu řetězu startu"], + 0, "Hlídá start, ne každé pozdější kliknutí."), + Q("Co ochrání data na odcizeném disku?", + ["Šifrování disku", "Jen lankový zámek skříně", + "Heslo spořiče", "Inventární štítek"], + 0, "Zámek zdrží odnesení celé skříně, ne čtení disku na jiném stroji."), + ]), + L("Nákup sestavy", "Buying a computer", + "Nejdřív práce, potom krabice.", + "The work comes first, then the box.", + [ + S("1 / 3 • Účel", "Učebna není střih videa", + "Tip: stejná sestava pro psaní a pro CAD je buď drahá, nebo pomalá.", + "Kancelář chce spolehlivost, ticho a dost paměti na prohlížeč.", + "Střih a CAD chtějí procesor, paměť a často grafiku.", + "Server chce ECC nebo aspoň diskové pole a správu, ne podsvícení.", + "Počet portů a dokování je součást ceny, ne doplněk potom."), + S("2 / 3 • Čísla", "Co srovnávat", + "Tip: více jader nepomůže programu, který umí jen jedno.", + "Paměť berte s rezervou na čtyři roky, ne na dnešní minimum.", + "SSD jako systémový disk je základ. Velký sklad může být pomalejší.", + "Zdroj má rezervu nad součtem štítků, ne jen přesný součet.", + "Záruka na místě je u učebny levnější než odvoz dvaceti strojů."), + S("3 / 3 • Stejnost", "Jedna image", + "Tip: dvacet různých desek znamená dvacet ovladačů.", + "Stejný model v učebně zjednoduší image i náhradní díl.", + "Dokoupit za dva roky stejnou desku často nejde. Pište, co je uvnitř.", + "Nejlevnější kus bez portů, které učebna používá, levný není.", + "Příkon a hluk patří do zadání, ne až do reklamace."), + ], + [ + Q("Podle čeho se volí sestava?", + ["Podle práce, kterou na ní škola dělá", + "Podle barvy krabice", + "Podle nejvyššího čísla v reklamě", + "Podle počtu RGB ventilátorů"], + 0, "CAD a psaní dopisu nechtějí stejné železo."), + Q("Proč v učebně stejný model?", + ["Jedna image a jeden náhradní díl", + "Aby se lišily ovladače", + "Aby každý stroj bootoval jinak", + "Zákon to vyžaduje kvůli VLAN"], + 0, "Různé desky znamenají různou údržbu."), + Q("Co u zdroje nestačí?", + ["Součet štítků bez rezervy", + "Rezerva nad typický odběr", + "Správný formát do skříně", + "Pojistka, která sedí k rozvodu"], + 0, "Špička a stárnutí se do přesného součtu nevejdou."), + Q("Co zdraží zdánlivě levný notebook?", + ["Chybějící porty a dok, který musíte dokoupit", + "To, že má SSD", + "To, že má záruku", + "To, že je tichý"], + 0, "Učebna bez obrazových výstupů potřebuje doky."), + ]), + L("Mobilní zařízení", "Mobile devices", + "Tablet a telefon ve školní síti.", + "Tablets and phones on the school network.", + [ + S("1 / 3 • Správa", "Cizí kapsy", + "Tip: telefon žáka není školní majetek a nepatří mu školní certifikát natrvalo.", + "Školní tablet se dá zapsat do správy a na dálku smazat.", + "Soukromý telefon do správy školy nepatří.", + "Účet školy na soukromém zařízení má jít odebrat.", + "Ztracený školní tablet bez správy je cizí počítač s vašimi daty."), + S("2 / 3 • Síť", "Kam ho pustit", + "Tip: telefon učitelů ve stejné VLAN jako servery je zbytečné riziko.", + "Hostovská nebo mobilní síť je oddělená.", + "Školní tablet může dostat jinou síť než návštěva.", + "Sdílené tablety se mezi žáky mažou nebo přepínají profil.", + "Nabíjení ve třídě je otázka pojistek, ne jen kabelů na stole."), + S("3 / 3 • Život", "Baterie a pád", + "Tip: cyklování baterie v nabíjecím kufru ji po dvou letech unaví.", + "Obal a popruh jsou součást nákupu, ne charita.", + "Aktualizace systému se dělají hromadně, ne podle nálady.", + "Rozbitý displej se nevrací do oběhu s trčícím sklem.", + "Evidence IMEI nebo sériového čísla patří k inventuře."), + ], + [ + Q("Čím se liší školní tablet od soukromého telefonu?", + ["Školní jde zapsat do správy a na dálku vymazat", + "Soukromý musí mít školní certifikát navždy", + "Jsou to stejné režimy", + "Tablet nepatří do inventury"], + 0, "Majetek školy se dá spravovat, kapsa žáka ne."), + Q("Kam nepatří učitelský telefon?", + ["Do VLAN se servery", + "Do oddělené mobilní sítě", + "Na účet, který jde odebrat", + "Mimo žákovská data, pokud to jde"], + 0, "Ztracený telefon nemá být klíč k serverům."), + Q("Co sdílený tablet mezi žáky potřebuje?", + ["Mazání profilu nebo oddělené účty", + "Jedno heslo napsané na obalu", + "Trvalou složku předchozího žáka", + "Vypnuté aktualizace"], + 0, "Cizí účet na dalším žákovi je únik."), + Q("Co patří do evidence tabletu?", + ["Sériové číslo a umístění", + "Jen barva obalu", + "Heslo žáka", + "Soukromé fotky"], + 0, "Bez čísla ho nepoznáte v kupě stejných."), + ]), + L("Periferie v provozu", "Peripherals in daily use", + "Tiskárny, projektory a co se na nich láme.", + "Printers, projectors, and what actually breaks.", + [ + S("1 / 3 • Projektor", "Lampa a síť", + "Tip: lampa má hodiny. Nečekejte, až praskne uprostřed hodiny.", + "Zdroj obrazu a kabel jsou častější chyba než deska.", + "Filtr projektoru se ucpe prachem a přístroj se vypne.", + "Stropní držák se dotahuje, nevěší se na něj.", + "Náhradní kabel ve třídě je levnější než přestávka."), + S("2 / 3 • Tisk", "Fronta a toner", + "Tip: prázdný toner a zaseknutý papír jsou dvě různé hlášky.", + "Sklad toneru má být suchý, ne na topení.", + "Účet tisku omezí náhodné desítky stran.", + "Síťová tiskárna se po výměně adresy musí najít znovu.", + "Servisní interval bubnu není totéž co výměna toneru."), + S("3 / 3 • Drobnosti", "Myš, sluchátka, kamera", + "Tip: bezdrátová myš bez náhradní baterie je v hodině mrtvá.", + "Sluchátka ve třídě mají být omyvatelná.", + "Kamera na dálkovou výuku chce světlo před obličejem, ne za zády.", + "USB přijímač myši ztracený v portu projektoru se špatně hledá.", + "Spotřební věci se objednávají dřív, než dojde poslední kus."), + ], + [ + Q("Co u projektoru kontrolovat dřív než desku?", + ["Zdroj obrazu, kabel a filtr", + "Jen typ žárovky ve stropě učebny", + "Heslo Wi-Fi", + "Verzi BIOSu učitelského telefonu"], + 0, "Obraz často končí na kabelu."), + Q("Čím se liší toner a buben?", + ["Toner je prach s barvou, buben je díl, který obraz přenáší", + "Jsou to totéž", + "Buben se mění každý den", + "Toner je jen u inkoustové tiskárny"], + 0, "Prázdný toner a unavený buben vypadají na stránce jinak."), + Q("Proč účty tisku?", + ["Omezí náhodné velké tisky", + "Zvýší rozlišení", + "Nahradí papír", + "Vypnou skener"], + 0, "Fronta bez limitu tiskne omylem celé knihy."), + Q("Co ve třídě dochází bez náhrady jako první?", + ["Baterie, kabely a spotřební díly", + "Procesory", + "Rackové lišty", + "TPM čipy"], + 0, "Drobnost zastaví hodinu stejně jako velká porucha."), + ]), + L("Ekologie a data", "Disposal and data", + "Vyhozený disk pořád obsahuje školu.", + "A discarded drive still contains the school.", + [ + S("1 / 3 • Disk", "Smazat neznamená vyhodit", + "Tip: rychlé formátování nechá data čitelná.", + "Před vyřazením se disk smaže tak, aby nešel běžně obnovit.", + "Nefunkční disk se fyzicky zničí, ne jen odloží.", + "SSD se maže příkazem, který buňky opravdu uvolní.", + "Potvrzení o likvidaci patří k inventuře."), + S("2 / 3 • Železo", "Není to směsný odpad", + "Tip: monitor a deska nepatří do popelnice za školou.", + "Elektroodpad se odevzdává na určené místo.", + "Baterie zvlášť, a nabobtnané ještě opatrněji.", + "Toner je taky odpad s režimem, ne koš ve sborovně.", + "Štítek s jménem žáka se před odvozem sundá."), + S("3 / 3 • Darování", "Starý stroj ven ze školy", + "Tip: darovaný počítač bez smazání účtů je dar i s přístupy.", + "Nejdřív data, potom majetek.", + "Licence vázaná na školu se nesmí odstěhovat s počítačem.", + "Zápis, komu stroj připadl, chrání školu.", + "Nefunkční dar je jen přesun odpadu."), + ], + [ + Q("Proč nestačí rychlé formátování?", + ["Data jdou často obnovit", + "Formát disk fyzicky rozbije", + "SSD formát zakazuje", + "Smaže i TPM v jiném počítači"], + 0, "Zmizí odkaz, ne obsah buněk."), + Q("Kam nepatří stará deska?", + ["Do směsného koše", + "Do elektroodpadu", + "K evidované likvidaci", + "Po smazání dat, pokud se vyřazuje"], + 0, "Elektroodpad má vlastní režim."), + Q("Co před darem počítače uděláte?", + ["Smažete data a ověříte licence", + "Necháte účty, ať se nový majitel přihlásí", + "Jen umyjete skříň", + "Přilepíte heslo na víko"], + 0, "Dar není předání školní sítě."), + Q("Čím se maže SSD líp než jedním přepisem naslepo?", + ["Příkazem, který buňky opravdu uvolní", + "Smazáním koše", + "Změnou tapety", + "Vypnutím Secure Boot"], + 0, "Řadič SSD přepis naslepo nemusí uložit tam, kde čekáte."), + ]), + L("Metodika závady", "A fault method", + "Jedna věta, která drží celý postup.", + "One sentence that holds the whole procedure.", + [ + S("1 / 3 • Příznak", "Co přesně nejde", + "Tip: ‚nejde počítač‘ může být černý obraz, špatné heslo i spadlá síť.", + "Zeptejte se, co uživatel čekal a co vidí.", + "Od kdy a jestli to jde jinde.", + "Jedna učebna, nebo jen jeden stůl.", + "Bez příznaku se mění díly naslepo."), + S("2 / 3 • Izolace", "Nejužší místo", + "Tip: fungující kabel a fungující port rozdělí chybu na dvě hromady.", + "Vyměňte kabel, port, účet nebo zařízení, vždy jen jedno.", + "Když náhradní kus funguje, původní je vinný nebo špatně nastavený.", + "Když nefunguje ani náhradní, chyba je ve společné věci.", + "Pište si mezivýsledky. Paměť po obědě lže."), + S("3 / 3 • Závěr", "Oprava a prevence", + "Tip: vyměněný kabel bez zápisu se příští měsíc tváří jako záhada.", + "Opravte příčinu, ne jen příznak na jednom stole.", + "Řekněte uživateli, co se změnilo.", + "Díly do opravy nepatří zpátky do skříně potichu.", + "Opakovaná závada chce úpravu nákupu nebo zapojení."), + ], + [ + Q("Co je první otázka?", + ["Co přesně nejde a od kdy", + "Jakou má deska barvu", + "Jestli je ve škole VLAN 20", + "Kolik stojí nový notebook"], + 0, "Příznak zužuje vrstvu, kterou budete zkoušet."), + Q("Proč měnit jen jeden díl?", + ["Ať víte, který rozdíl pomohl", + "Sklad má jen jeden kabel", + "BIOS víc výměn zakazuje", + "Uživatel jinak zapomene heslo"], + 0, "Dvě výměny naráz nic nedokážou."), + Q("Náhradní kabel taky nejde. Co z toho plyne?", + ["Chyba je spíš ve portu, účtu nebo zařízení", + "Oba kabely jsou vždy špatné", + "Síť nemá smysl", + "Je rozbitý TPM"], + 0, "Společné selhání ukazuje na společnou část."), + Q("Co patří do zápisu opravy?", + ["Příznak, příčina a co se vyměnilo", + "Jen ‚hotovo‘", + "Heslo uživatele", + "Soukromý názor na učitele"], + 0, "Příště podle zápisu nezačínáte od nuly."), + ]), + L("Opakování k maturitě", "Exam review", + "Železo, které umíte pojmenovat a zdůvodnit.", + "Hardware you can name and justify.", + [ + S("1 / 3 • Tok dat", "Od kliknutí k disku", + "Tip: maturita chce cestu, ne seznam zkratek.", + "Vstup, paměť, procesor, sběrnice, disk a výstup.", + "RAM je pracovní, disk je trvalý. Není to totéž.", + "Grafika může mít vlastní paměť a pořád jede po sběrnici.", + "Úzké hrdlo je nejpomalejší článek této cesty."), + S("2 / 3 • Spolehlivost", "Co drží učebnu", + "Tip: záloha, RAID a UPS řeší tři různé nehody.", + "RAID drží provoz při pádu disku.", + "Záloha vrátí data z jiného místa a času.", + "UPS dá čas na vypnutí.", + "Firmware a čištění jsou prevence, ne oprava po požáru."), + S("3 / 3 • Rozhodnutí", "Proč právě tahle krabice", + "Tip: umíte obhájit paměť, disk a porty pro konkrétní učebnu.", + "Kancelář, střih a server mají jiné priority.", + "Stejný model ve třídě šetří image.", + "Vyřazení začíná smazáním dat.", + "Závadu vedete od příznaku po jedné změně."), + ], + [ + Q("Čím se liší RAM a disk?", + ["RAM je pracovní paměť, disk drží data i bez napájení", + "Jsou to totéž", + "Disk je vždy rychlejší", + "RAM se zálohuje na pásku"], + 0, "Vypnutí smaže obsah RAM, ne uložený soubor na disku."), + Q("Co vrátí soubor smazaný před týdnem?", + ["Záloha z dřívějšího dne, ne RAID", + "RAID 1 sám", + "UPS", + "Secure Boot"], + 0, "RAID má pořád aktuální, tedy i smazaný stav."), + Q("Co dá UPS?", + ["Čas na pořádné vypnutí", + "Kopii dat v jiné budově", + "Rychlejší procesor", + "Nový disk"], + 0, "Baterie není archiv."), + Q("Čím začíná vyřazení počítače?", + ["Smazáním dat", + "Sundáním obalu", + "Darem bez kontroly", + "Zápisem do VLAN"], + 0, "Nejdřív obsah, potom plech."), + ]), + ], +) + +COURSES = [NET2, NET3, NET4, HW2, HW3, HW4] +BACK = { + "net2": "netyears", "net3": "netyears", "net4": "netyears", + "hw2": "hwyears", "hw3": "hwyears", "hw4": "hwyears", +} +TITLE = { + "net2": "net_year2", "net3": "net_year3", "net4": "net_year4", + "hw2": "hw_year2", "hw3": "hw_year3", "hw4": "hw_year4", +} +PTR = { + "net2": "qy_net2", "net3": "qy_net3", "net4": "qy_net4", + "hw2": "qy_hw2", "hw3": "qy_hw3", "hw4": "qy_hw4", +} + + +def emit(): + lines = [ + '#include "graduately.h"', + "", + "/* Generated by scripts/gen-it-years.py. Do not edit by hand. */", + "", + ] + i18n = [] + pages = [] + for course in COURSES: + p = course["prefix"] + i18n.append( + f' {{"{p}_sub", {c(course["sub"])}, {c(course["sub_en"])}}},' + ) + for n, lesson in enumerate(course["lessons"], start=1): + i18n.append( + f' {{"{p}_unit{n}", {c(lesson["title"])}, {c(lesson["title_en"])}}},' + ) + i18n.append( + f' {{"{p}_unit{n}_sub", {c(lesson["sub"])}, {c(lesson["sub_en"])}}},' + ) + i18n.append( + f' {{"{p}_ex{n}_title", {c("Kvíz: " + lesson["title"])}, ' + f'{c("Quiz: " + lesson["title_en"])}}},' + ) + i18n.append( + f' {{"{p}_quiz{n}_head", {c("Kvíz k tématu " + lesson["title"])}, ' + f'{c("Quiz on " + lesson["title_en"])}}},' + ) + lines.append(f"static GtkWidget *build_{p}_unit{n}_page(void) {{") + lines.append(" static const NetSlide slides[] = {") + for kicker, title, tip, bullets in lesson["slides"]: + lines.append(" {") + lines.append(f" {c(kicker)}, {c(title)}, {c(tip)},") + lines.append(" {") + for bullet in bullets: + lines.append(f" {c(bullet)},") + lines.append(" NULL,") + lines.append(" },") + lines.append(" },") + lines.append(" };") + lines.append( + f' return quizyear_unit({PTR[p]}, {n - 1}, "{p}_unit{n}", ' + f'"{p}_unit{n}_sub", slides, (int)(sizeof slides / sizeof slides[0]));' + ) + lines.append("}") + lines.append("") + lines.append(f"static GtkWidget *build_{p}_unit{n}_exercise_page(void) {{") + lines.append(" static const ChoiceQ qs[] = {") + for prompt, options, correct, _hint in lesson["quiz"]: + opts = ", ".join(c(o) for o in options) + lines.append(f" {{{c(prompt)}, {{{opts}}}, 4, {correct}}},") + lines.append(" };") + lines.append(" static const char *hints[] = {") + for _prompt, _options, _correct, hint in lesson["quiz"]: + lines.append(f" {c(hint)},") + lines.append(" };") + lines.append( + f' return quizyear_quiz({PTR[p]}, {n - 1}, "{p}_ex{n}_title", ' + f'"{p}_quiz{n}_head", qs, hints, (int)(sizeof qs / sizeof qs[0]));' + ) + lines.append("}") + lines.append("") + pages.append((f"{p}unit{n}", f"build_{p}_unit{n}_page")) + pages.append((f"{p}ex{n}", f"build_{p}_unit{n}_exercise_page")) + + lines.append("void add_it_year_pages(GtkStack *stack) {") + lines.append(" quizyear_boot();") + for course in COURSES: + p = course["prefix"] + lines.append( + f' gtk_stack_add_named(stack, quizyear_map({PTR[p]}), "{p}map");' + ) + for name, fn in pages: + lines.append(f' gtk_stack_add_named(stack, {fn}(), "{name}");') + lines.append("}") + lines.append("") + + (ROOT / "src" / "it_year_pages.c").write_text("\n".join(lines) + "\n", encoding="utf-8") + (ROOT / "src" / "it_years_i18n.inc").write_text("\n".join(i18n) + "\n", encoding="utf-8") + print(f"wrote {len(COURSES) * 8} lessons") + + +if __name__ == "__main__": + emit() diff --git a/src/graduately.h b/src/graduately.h index cebb6de..d131c52 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -946,6 +946,26 @@ GtkWidget *build_dehome_page(void); void add_de_pages(GtkStack *stack); void de_load_progress(void); void progress_for_de(ProgressSum *out); +typedef struct QuizYear QuizYear; +extern QuizYear *qy_net2; +extern QuizYear *qy_net3; +extern QuizYear *qy_net4; +extern QuizYear *qy_hw2; +extern QuizYear *qy_hw3; +extern QuizYear *qy_hw4; +void quizyear_boot(void); +void quizyear_load_all(void); +void quizyear_apply_lang(void); +void progress_for_net_years(ProgressSum *out); +void progress_for_hw_years(ProgressSum *out); +GtkWidget *quizyear_map(QuizYear *Y); +GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key, + const char *sub_key, const NetSlide *slides, + int n_slides); +GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key, + const char *head_key, const ChoiceQ *qs, + const char **hints, int n); +void add_it_year_pages(GtkStack *stack); void on2_load_progress(void); void on2_save_progress(void); void mark_on2_done(int lesson_id); diff --git a/src/hw.c b/src/hw.c index 2029953..45760e6 100644 --- a/src/hw.c +++ b/src/hw.c @@ -383,8 +383,21 @@ static GtkWidget *hw_year_button(int year, gboolean locked) { gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); gtk_box_append(GTK_BOX(row), icon); } else { + static const char *pages[] = { + "hwmap", "hw2map", "hw3map", "hw4map", + }; + static const char *subs[] = { + "hw_sub", "hw2_sub", "hw3_sub", "hw4_sub", + }; + GtkWidget *sub = gtk_label_new(NULL); + + i18n_bind(sub, subs[year - 1], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_widget_add_css_class(sub, "quiz-intro"); + gtk_box_append(GTK_BOX(texts), sub); g_object_set_data_full(G_OBJECT(btn), "target", - g_strdup("hwmap"), g_free); + g_strdup(pages[year - 1]), g_free); g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); } @@ -422,7 +435,7 @@ GtkWidget *build_hwyears_page(void) { gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); for (y = 1; y <= 4; y++) - gtk_box_append(GTK_BOX(list), hw_year_button(y, y > 1)); + gtk_box_append(GTK_BOX(list), hw_year_button(y, FALSE)); return page; } diff --git a/src/i18n.c b/src/i18n.c index 18ece05..3d4b562 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -1621,6 +1621,7 @@ const TrEntry tr_ui[] = { "Drag the story parts into the correct order."}, #include "english_i18n.inc" #include "german_i18n.inc" +#include "it_years_i18n.inc" #include "math_i18n.inc" {NULL, NULL, NULL} }; @@ -2605,6 +2606,7 @@ void on3_lessons_apply_lang(void); void on4_lessons_apply_lang(void); void en_lessons_apply_lang(void); void de_lessons_apply_lang(void); +void quizyear_apply_lang(void); void lit_lessons_apply_lang(void); void sci_lessons_apply_lang(void); void fyz_lessons_apply_lang(void); @@ -2629,6 +2631,7 @@ void apply_language(void) { on4_lessons_apply_lang(); en_lessons_apply_lang(); de_lessons_apply_lang(); + quizyear_apply_lang(); lit_lessons_apply_lang(); sci_lessons_apply_lang(); fyz_lessons_apply_lang(); diff --git a/src/it_year_pages.c b/src/it_year_pages.c new file mode 100644 index 0000000..764012e --- /dev/null +++ b/src/it_year_pages.c @@ -0,0 +1,2606 @@ +#include "graduately.h" + +/* Generated by scripts/gen-it-years.py. Do not edit by hand. */ + +static GtkWidget *build_net2_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • VLAN", "Oddělení provozu", "Tip: VLAN není samostatný kabel, je to značka v rámci.", + { + "VLAN sdružuje porty do jedné vysílací domény.", + "Počítače v různých VLAN spolu přímo na druhé vrstvě nemluví.", + "Číslo VLAN se na trunku vozí ve značce 802.1Q.", + "Nativní VLAN na trunku jede neznačkovaná. Na obou koncích musí sedět.", + NULL, + }, + }, + { + "2 / 3 • Porty", "Access a trunk", "Tip: access port patří do jedné VLAN, trunk jich nese víc.", + { + "Access port se připojuje ke koncovému zařízení.", + "Trunk spojuje přepínače nebo přepínač s routerem.", + "Povolené VLAN na trunku omezí, co smí přejít.", + "Špatně nastavený trunk pustí cizí VLAN do špatné sítě.", + NULL, + }, + }, + { + "3 / 3 • Provoz", "K čemu se VLAN hodí", "Tip: oddělení hostů od školy nesmí stát jen na hesle Wi-Fi.", + { + "Hosté, škola a správa mohou sdílet jeden přepínač.", + "Hlasová VLAN oddělí telefony od dat.", + "Mezi VLAN se dostane jen směrovač nebo přepínač třetí vrstvy.", + "VLAN zmenšuje broadcast, nenahrazuje firewall.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 0, "net2_unit1", "net2_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co VLAN na přepínači dělá?", {"Sdruží porty do jedné vysílací domény", "Zrychlí procesor serveru", "Nahradí šifrování WPA3", "Přidělí veřejnou IPv4 adresu"}, 4, 0}, + {"Jak se VLAN pozná na trunku?", {"Značkou 802.1Q v rámci", "Jinou MAC adresou zdroje", "Jiným konektorem RJ11", "Bitem v ICMP"}, 4, 0}, + {"Kam patří access port?", {"Ke koncovému počítači v jedné VLAN", "Jen mezi dvěma routery", "Na optický spoj s vlnovým multiplexem", "Do napájecího zdroje"}, 4, 0}, + {"Jak spolu mluví dvě různé VLAN?", {"Přes směrování, ne přímo na druhé vrstvě", "Samy od sebe, VLAN je jen barva kabelu", "Jedině přes ARP broadcast do všech VLAN", "Jen když mají stejnou MAC adresu"}, 4, 0}, + }; + static const char *hints[] = { + "VLAN odděluje provoz na druhé vrstvě.", + "802.1Q vloží do ethernetového rámce číslo VLAN.", + "Access port má jednu VLAN a značku obvykle sundá.", + "Mezi VLAN je potřeba router nebo L3 switch.", + }; + return quizyear_quiz(qy_net2, 0, "net2_ex1_title", "net2_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Smyčka", "Broadcastová bouře", "Tip: na druhé vrstvě není TTL, rámec může kroužit donekonečna.", + { + "Dva kabely mezi stejnými přepínači vytvoří smyčku.", + "Broadcast se kopíruje na všechny porty a vrátí se zpět.", + "Přepínač se zahltí a přestane přenášet užitečný provoz.", + "Na třetí vrstvě paket TTL snižuje, ethernetový rámec ne.", + NULL, + }, + }, + { + "2 / 3 • STP", "Jedna aktivní cesta", "Tip: STP nevybírá nejrychlejší cestu pro data, brání smyčce.", + { + "Přepínače si volí root bridge, obvykle nejnižší bridge ID.", + "Některé porty zůstanou ve stavu blocking a rámce nepřeposílají.", + "Když aktivní linka spadne, blocking port se může otevřít.", + "RSTP reaguje rychleji než původní STP.", + NULL, + }, + }, + { + "3 / 3 • Ochrana", "Porty, které nemají přepínat", "Tip: port k tiskárně nemá poslouchat cizí BPDU.", + { + "PortFast na access portu zrychlí připojení počítače.", + "BPDU Guard port shodí, když na něm přijde BPDU.", + "Root Guard brání tomu, aby se cizí switch stal rootem.", + "Záložní kabel má smysl, ale jen se zapnutým STP.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 1, "net2_unit2", "net2_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč je smyčka na přepínačích nebezpečná?", {"Broadcast může kroužit bez omezení TTL", "IPv4 přestane mít masku", "DNS přejmenuje domény", "Procesor v počítači se vypne"}, 4, 0}, + {"Co STP s nadbytečným portem udělá?", {"Nechá ho blokovat, aby nevznikla smyčka", "Zdvojnásobí rychlost obou kabelů", "Smaže MAC tabulku navždy", "Přesměruje port na VLAN 1 a vypne ostatní"}, 4, 0}, + {"K čemu je BPDU Guard?", {"Shodí access port, na kterém se objeví cizí přepínač", "Šifruje heslo k Wi-Fi", "Přiděluje IPv6 adresy", "Měří útlum optiky"}, 4, 0}, + {"Které tvrzení o RSTP sedí?", {"Na výpadek reaguje rychleji než původní STP", "Ruší potřebu VLAN", "Funguje jen na IPv6", "Nahrazuje směrovací tabulku"}, 4, 0}, + }; + static const char *hints[] = { + "Ethernetový rámec nemá TTL.", + "Blocking port rámce nepřeposílá, dokud není potřeba.", + "Na portu pro počítač nemá co dělat protokol přepínačů.", + "RSTP zkracuje čekání při změně topologie.", + }; + return quizyear_quiz(qy_net2, 1, "net2_ex2_title", "net2_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • DHCP", "Adresa bez opisování", "Tip: DHCP dává adresu na čas, ne navždy.", + { + "Klient pošle broadcast a hledá server.", + "Nabídka obsahuje adresu, masku, bránu a DNS.", + "Zapůjčení má dobu platnosti a klient ji obnovuje.", + "Rezervace přiváže adresu ke konkrétní MAC adrese.", + NULL, + }, + }, + { + "2 / 3 • Rozsah", "Co se nesmí překrývat", "Tip: dva servery se stejnou adresou v jedné síti si škodí.", + { + "Rozsah musí ležet v síti, kterou má brána.", + "Statické adresy serverů do rozsahu nepatří, nebo se vyloučí.", + "Špatná brána v nabídce odřízne celou VLAN od internetu.", + "DHCP relay přenese požadavek z VLAN, kde server není.", + NULL, + }, + }, + { + "3 / 3 • DNS", "Jméno na adresu", "Tip: DNS není totéž co DHCP. Jedno dává adresu, druhé jméno.", + { + "A záznam ukazuje na IPv4, AAAA na IPv6.", + "Reverzní PTR překládá adresu zpět na jméno.", + "Rekurzivní resolver se ptá dál, autoritativní server zónu vlastní.", + "TTL u záznamu říká, jak dlouho se smí odpověď cacheovat.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 2, "net2_unit3", "net2_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co klient od DHCP obvykle dostane?", {"Adresu, masku, bránu a DNS", "Jen heslo k Wi-Fi", "Novou MAC adresu", "Kořenový certifikát webu"}, 4, 0}, + {"Proč se rezervace v DHCP hodí?", {"Zařízení s danou MAC dostane pořád stejnou adresu", "Vypne potřebu masky", "Nahradí směrování", "Zašifruje disk"}, 4, 0}, + {"Čím se liší A a AAAA?", {"A je IPv4, AAAA je IPv6", "A je pošta, AAAA je web", "A je jméno VLAN, AAAA je číslo portu", "Oba ukazují jen na IPv4"}, 4, 0}, + {"K čemu je DHCP relay?", {"Pošle požadavek z jiné VLAN k serveru", "Překládá jména bez DNS", "Blokuje STP", "Mění rychlost portu na 10 Gb/s"}, 4, 0}, + }; + static const char *hints[] = { + "To jsou čtyři údaje, bez kterých stanice v síti nepracuje.", + "Rezervace je vhodná pro tiskárny a kamery.", + "Čtyři A v názvu připomínají delší IPv6 adresu.", + "Broadcast DHCP jinak VLAN neopustí.", + }; + return quizyear_quiz(qy_net2, 2, "net2_ex3_title", "net2_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • NAT", "Soukromé adresy ven", "Tip: 192.168.0.0/16 a 10.0.0.0/8 na internetu směrovat nejdou.", + { + "NAT přepíše zdrojovou adresu při odchodu z lokální sítě.", + "Zpět router podle tabulky překladů provoz vrátí.", + "Zvenku nikdo sám od sebe spojení dovnitř nenaváže.", + "To je vedlejší efekt, ne náhrada firewallu.", + NULL, + }, + }, + { + "2 / 3 • PAT", "Mnoho počítačů, jedna adresa", "Tip: PAT si pamatuje i číslo portu, ne jen adresu.", + { + "Více vnitřních adres sdílí jednu veřejnou.", + "Rozliší je zdrojový port.", + "Domácí router dělá téměř vždy PAT, ne čistý NAT jedna ku jedné.", + "Dojdou-li porty, nová spojení se neotevřou.", + NULL, + }, + }, + { + "3 / 3 • Dovnitř", "Když má server být vidět", "Tip: přesměrování portu otevře jen tu službu, kterou opravdu chcete.", + { + "Port forward pošle příchozí port na vnitřní adresu.", + "Na serveru pak musí poslouchat daná služba a firewall ji pustit.", + "DMZ na domácím routeru vystaví jeden počítač hodně otevřeně.", + "Lepší je konkrétní port než celá DMZ.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 3, "net2_unit4", "net2_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč stanice s adresou 192.168.1.20 není z internetu vidět přímo?", {"Je to soukromá adresa a ven jde až přes NAT", "IPv4 takové adresy nezná", "MAC adresa se na internetu nesmí použít jako jméno", "DNS soukromé adresy maže"}, 4, 0}, + {"Čím se PAT liší od NAT jedna ku jedné?", {"Více vnitřních adres sdílí jednu veřejnou a liší se portem", "PAT šifruje provoz", "PAT funguje jen pro IPv6", "PAT ruší potřebu brány"}, 4, 0}, + {"K čemu je přesměrování portu?", {"Příchozí spojení na daný port pošle na vnitřní server", "Přidělí VLAN", "Zapne STP", "Změní masku celé školy na /8"}, 4, 0}, + {"Proč NAT není firewall?", {"Jen překládá adresy, pravidla provozu řeší firewall", "NAT umí totéž co antivir", "NAT kontroluje hesla uživatelů", "NAT šifruje Wi-Fi"}, 4, 0}, + }; + static const char *hints[] = { + "Soukromé rozsahy se mezi poskytovateli nesměrují.", + "Překlad zahrnuje adresu i port.", + "Bez něj zvenku na vnitřní web sami nedosáhnete.", + "Nechtěný publikovaný port NAT klidně pustí.", + }; + return quizyear_quiz(qy_net2, 3, "net2_ex4_title", "net2_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Adresa", "128 bitů", "Tip: nuly v jedné skupině za sebou smí zkrátit :: jen jednou.", + { + "Adresa má osm skupin po čtyřech hexadecimálních číslicích.", + "Prefix se píše jako /64, ne jako stará maska 255.255.255.0.", + "Link-local adresa fe80:: platí jen na tom spoji.", + "Globální adresa je vidět i mimo místní spoj.", + NULL, + }, + }, + { + "2 / 3 • Přidělení", "SLAAC a DHCPv6", "Tip: výchozí bránu v IPv6 často ohlašuje router, ne DHCP.", + { + "Router advertisement řekne prefix a jestli se stanice smí nastavit sama.", + "SLAAC složí adresu z prefixu a identifikátoru rozhraní.", + "DHCPv6 přidá adresu nebo jen DNS, podle příznaků.", + "Obě varianty mohou běžet vedle sebe.", + NULL, + }, + }, + { + "3 / 3 • Vedle IPv4", "Dual stack", "Tip: IPv6 nevypínejte jen proto, že mu nerozumíte. Rozbitý IPv6 zpomalí i weby.", + { + "Počítač může mít IPv4 i IPv6 naráz.", + "DNS AAAA záznam rozhoduje, jestli se klient zkusí spojit po IPv6.", + "NAT u IPv6 není nutný tak jako u IPv4, ale firewall ano.", + "Ping na IPv6 je jiný příkaz než ping na IPv4, podle systému.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 4, "net2_unit5", "net2_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jak je IPv6 adresa dlouhá?", {"128 bitů", "32 bitů", "48 bitů", "256 bitů"}, 4, 0}, + {"Která adresa je link-local?", {"fe80::1", "192.168.1.1", "8.8.8.8", "255.255.255.255"}, 4, 0}, + {"Kdo v IPv6 často oznámí prefix?", {"Router advertisement", "ARP request", "STP hello", "Jen ruční zápis do hosts"}, 4, 0}, + {"Co znamená dual stack?", {"Zařízení používá IPv4 i IPv6", "Dva switche v jedné VLAN", "Dvě masky u jedné IPv4 adresy", "Záloha jen na pásku"}, 4, 0}, + }; + static const char *hints[] = { + "IPv4 má 32 bitů, IPv6 čtyřikrát tolik.", + "fe80:: platí jen na místním spoji.", + "Router říká stanicím, jaký prefix na spoji platí.", + "Oba protokoly běží současně.", + }; + return quizyear_quiz(qy_net2, 4, "net2_ex5_title", "net2_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Trasa", "Kam s paketem", "Tip: směrovací tabulka se dívá na síť, ne na jednu adresu stanice.", + { + "Záznam říká síť, masku nebo prefix a další skok.", + "Další skok musí být dosažitelný na přímo připojeném rozhraní.", + "Výchozí trasa 0.0.0.0/0 chytí vše, co nemá přesnější záznam.", + "Přesnější prefix vyhraje nad kratším.", + NULL, + }, + }, + { + "2 / 3 • Metrika", "Když je cest víc", "Tip: nižší administrativní vzdálenost znamená důvěryhodnější zdroj trasy.", + { + "Statická trasa má lepší vzdálenost než OSPF nebo RIP.", + "Metrika porovnává trasy ze stejného zdroje.", + "Plovoucí statická trasa má horší vzdálenost a čeká jako záloha.", + "Špatný další skok vytvoří černou díru: paket odejde a ztratí se.", + NULL, + }, + }, + { + "3 / 3 • Meze", "Kdy ruční tabulka nestačí", "Tip: deset poboček se statickými trasami neudržíte po každé změně.", + { + "Statika je přehledná v malé síti a na okraji k poskytovateli.", + "Neumí sama obejít spadlou cestu, pokud záloha není připravená.", + "Na směrovači musí sedět trasa tam i zpět, jinak odpověď nepřijde.", + "Změna adresace znamená obejít všechny ruční záznamy.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 5, "net2_unit6", "net2_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Na co se směrovač dívá při výběru trasy?", {"Na síť a prefix, ne na jméno počítače", "Na barvu kabelu", "Na VLAN jméno v DNS", "Jen na MAC adresu cíle"}, 4, 0}, + {"Která trasa je výchozí?", {"0.0.0.0/0", "255.255.255.255/32", "192.168.1.1/32", "::1/128"}, 4, 0}, + {"Co je plovoucí statická trasa?", {"Záložní ruční trasa s horší vzdáleností", "Trasa, která plave mezi VLAN bez routeru", "Automatický OSPF", "Záznam jen v ARP"}, 4, 0}, + {"Proč odpověď ze serveru nedorazí, i když tam paket došel?", {"Chybí zpáteční trasa", "TCP nemá porty", "IPv4 neumí odpovědi", "Switch maže cílové MAC adresy"}, 4, 0}, + }; + static const char *hints[] = { + "Třetí vrstva směruje podle prefixu.", + "Nulová síť s nulovým prefixem chytí zbytek.", + "Nastoupí, až když zmizí lepší cesta.", + "Cesta musí existovat oběma směry.", + }; + return quizyear_quiz(qy_net2, 5, "net2_ex6_title", "net2_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Pravidla", "Pořadí rozhoduje", "Tip: první shoda vyhrává. Obecné povolení na začátku pozdější zákaz schová.", + { + "ACL zkouší řádky shora dolů.", + "Na konci bývá tiché zamítnutí všeho ostatního.", + "Pravidlo umí adresu, protokol a port.", + "Jedno špatné pořadí otevře službu, kterou jste chtěli zavřít.", + NULL, + }, + }, + { + "2 / 3 • Směr", "Dovnitř a ven", "Tip: blíž zdroji filtrujte to, co nemá vůbec vstoupit.", + { + "Příchozí ACL sedí na paketech, které do rozhraní vcházejí.", + "Odchozí ACL sedí na paketech, které rozhraní opouštějí.", + "Filtr na VLAN rozhraní hlídá provoz mezi sítěmi.", + "ACL na směrovači nevidí provoz, který zůstane uvnitř jednoho switche.", + NULL, + }, + }, + { + "3 / 3 • Praxe", "Co obvykle pustit", "Tip: nejdřív povolte, co potřebujete, a zbytek nechte spadnout.", + { + "Správa směrovače nemá poslouchat z hostovské VLAN.", + "Do serverové sítě pusťte jen porty služeb, které tam jsou.", + "ICMP můžete omezit, ale úplný zákaz ztíží hledání chyb.", + "Po změně ACL ověřte, že legitimní provoz pořád prochází.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 6, "net2_unit7", "net2_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jak ACL vybírá pravidlo?", {"Použije první řádek, který sedí", "Sečte všechny řádky a udělá průměr", "Vždy použije poslední řádek", "Ptá se DNS"}, 4, 0}, + {"Co udělá tiché zamítnutí na konci?", {"Zahodí provoz, který žádný řádek nepovolil", "Povolí všechno, co zbylo", "Přepne port do trunku", "Pošle paket do IPv6"}, 4, 0}, + {"Proč ACL na routeru nehlídá dva počítače v jedné VLAN?", {"Jejich rámce jdou jen přes switch a k routeru nemusí", "ACL umí jen Wi-Fi", "VLAN nemá MAC adresy", "TCP takový provoz šifruje před switchem"}, 4, 0}, + {"Kam nepatří správa routeru?", {"Do hostovské sítě", "Na konzolový port technika", "Do správní VLAN", "Na šifrované SSH z dohledu"}, 4, 0}, + }; + static const char *hints[] = { + "Pořadí je součást významu seznamu.", + "Co není povoleno, neprojde.", + "Provoz uvnitř VLAN zůstane na druhé vrstvě.", + "Host nemá mít cestu na správu infrastruktury.", + }; + return quizyear_quiz(qy_net2, 6, "net2_ex7_title", "net2_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net2_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • SSID", "Více sítí, jedna infrastruktura", "Tip: hostovské SSID má skončit v jiné VLAN než učitelé.", + { + "Jedno rádio může ohlašovat víc SSID.", + "Každé SSID se mapuje na VLAN.", + "Schované SSID není zabezpečení, jméno jde pořád zachytit.", + "Stejné SSID na více přístupových bodech umožní přechod.", + NULL, + }, + }, + { + "2 / 3 • Klíče", "WPA2 a WPA3", "Tip: společné heslo PSK se nedá vzít jednomu člověku, aniž by ho dostali všichni.", + { + "WPA2-Personal sdílí jedno heslo.", + "WPA2-Enterprise ověřuje uživatele přes RADIUS.", + "WPA3 lépe chrání i slabší hesla a má lepší dopředné utajení.", + "Starý WEP se nepoužívá.", + NULL, + }, + }, + { + "3 / 3 • Pokrytí", "Kanály a přechod", "Tip: dva sousední body na stejném kanále si překážejí.", + { + "Pásmo 2,4 GHz má málo nepřekrývajících se kanálů.", + "5 GHz má víc kanálů a hůř prochází zdí.", + "Výkon nesmí být zbytečně vysoký, jinak buňky slyší i cizí provoz.", + "Roaming funguje, když se SSID a zabezpečení shodují.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net2, 7, "net2_unit8", "net2_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net2_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kam má vést hostovské SSID?", {"Do vlastní VLAN, oddělené od školní sítě", "Do stejné sítě jako ředitelna", "Přímo do správy routeru", "Do sítě bez DHCP i bez izolace"}, 4, 0}, + {"Čím se liší WPA2-Enterprise od společného hesla?", {"Každý uživatel se ověřuje zvlášť, často přes RADIUS", "Nemá šifrování", "Funguje jen na 2,4 GHz", "Sdílí jeden klíč napsaný na tabuli"}, 4, 0}, + {"Proč schované SSID není ochrana?", {"Jméno sítě jde při připojení stejně zachytit", "Skryté SSID nejde vůbec použít", "WPA3 ho zakazuje", "Switch ho vyhlásí v STP"}, 4, 0}, + {"Co vadí dvěma sousedním AP na stejném kanále?", {"Slyší se a kazí si přenos", "Smažou si VLAN", "Přestanou dělat DHCP", "Změní IPv6 na IPv4"}, 4, 0}, + }; + static const char *hints[] = { + "Host nemá vidět školní servery.", + "Účet jde odebrat jednomu člověku.", + "Utajené jméno není šifra.", + "Kanály se mají plánovat, ne nechat náhodě.", + }; + return quizyear_quiz(qy_net2, 7, "net2_ex8_title", "net2_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Sousedé", "Stejná oblast", "Tip: OSPF soused nevznikne, když se neshoduje oblast nebo hello.", + { + "Směrovače v jednom spoji si vymění hello.", + "Hello interval a dead interval musí sedět.", + "Oblast 0 je páteř. Ostatní oblasti se k ní připojují.", + "Pasivní rozhraní hello neposílá, třeba k počítačům.", + NULL, + }, + }, + { + "2 / 3 • Cena", "Kratší neznamená míň skoků", "Tip: OSPF sčítá cenu linek, ne počet routerů jako RIP.", + { + "Rychlejší linka má obvykle nižší cenu.", + "Součet cen rozhodne, kudy paket půjde.", + "Ruční cena umí odtáhnout provoz ze špatné linky.", + "Změna topologie se přepočítá bez přepisování statických tras.", + NULL, + }, + }, + { + "3 / 3 • Tabulka", "Co se z OSPF stane", "Tip: v tabulce uvidíte zdroj trasy, ne jen další skok.", + { + "Naučená síť se vloží do směrovací tabulky.", + "Když lepší cesta spadne, použije se náhradní.", + "Redistribuce natáhne do OSPF i statiku nebo jiný protokol.", + "Špatná redistribuce umí do sítě pustit cizí trasy.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 0, "net3_unit1", "net3_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co musí u OSPF sousedů sedět?", {"Oblast a časy hello", "Stejná MAC adresa", "Stejné SSID", "Stejný počet VLAN na switchi"}, 4, 0}, + {"Podle čeho OSPF vybírá cestu?", {"Podle součtu cen linek", "Podle počtu písmen v názvu routeru", "Podle VLAN ID", "Podle stáří DNS záznamu"}, 4, 0}, + {"K čemu je pasivní rozhraní?", {"Neposílá hello tam, kde žádný soused být nemá", "Vypne směrování celé sítě", "Zapne NAT", "Skryje SSID"}, 4, 0}, + {"Co je oblast 0?", {"Páteř, ke které se ostatní oblasti připojují", "VLAN pro hosty", "Výchozí heslo WPA", "Číslo konzolového portu"}, 4, 0}, + }; + static const char *hints[] = { + "Nesoulad hello nebo oblasti sousedství nesestaví.", + "Cena vyjadřuje náklad cesty, často odvozený z rychlosti.", + "K uživatelům se hello zbytečně neposílá.", + "Bez páteře se víc oblastí OSPF nespojí správně.", + }; + return quizyear_quiz(qy_net3, 0, "net3_ex1_title", "net3_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Router na tyči", "Jeden spoj, víc VLAN", "Tip: trunk k routeru nese značky a každá VLAN má své podrozhraní.", + { + "Switch pošle trunk do routeru.", + "Každá VLAN má vlastní IP bránu.", + "Stanice má jako bránu adresu své VLAN, ne cizí.", + "Bez trasy mezi VLAN k sobě sítě nedosáhnou.", + NULL, + }, + }, + { + "2 / 3 • L3 switch", "Směrování v přepínači", "Tip: přepínač třetí vrstvy má pro VLAN rozhraní SVI.", + { + "SVI je virtuální rozhraní s adresou brány.", + "Provoz mezi VLAN nemusí odejít na samostatný router.", + "ACL na SVI hlídá, která VLAN kam smí.", + "Zapomenuté SVI znamená, že VLAN nemá bránu.", + NULL, + }, + }, + { + "3 / 3 • Chyby", "Když to nejde", "Tip: nejdřív ověřte, že stanice je ve správné VLAN a má správnou bránu.", + { + "Špatná maska na bráně rozhodí celou VLAN.", + "Trunk, který VLAN nepropouští, bránu odstřihne.", + "Stanice v access portu špatné VLAN dostane cizí DHCP.", + "Kontrola: adresa, maska, brána, VLAN, trunk, ACL.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 1, "net3_unit2", "net3_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je brána pro stanici ve VLAN?", {"Adresa routeru nebo SVI v té samé VLAN", "Libovolná veřejná DNS adresa", "MAC adresa switche", "Číslo VLAN zapsané do masky"}, 4, 0}, + {"Co je SVI?", {"Virtuální rozhraní VLAN na L3 přepínači", "Typ optického vlákna", "Protokol pošty", "Záloha konfigurace"}, 4, 0}, + {"Jak router na tyči pozná VLAN?", {"Podle značky 802.1Q na trunku", "Podle barvy LED na počítači", "Podle jména v DNS", "Podle rychlosti ventilátoru"}, 4, 0}, + {"Co udělá trunk, který VLAN 20 nepropouští?", {"Střihne bránu VLAN 20 od zbytku sítě", "Smaže OSPF v celé škole", "Přepne Wi-Fi na WEP", "Zdvojí adresy DHCP"}, 4, 0}, + }; + static const char *hints[] = { + "Brána musí být dosažitelná na druhé vrstvě, tedy ve stejné VLAN.", + "SVI nese IP adresu brány dané VLAN.", + "Každé podrozhraní patří jedné značce.", + "Povolené VLAN na trunku jsou součást návrhu.", + }; + return quizyear_quiz(qy_net3, 1, "net3_ex2_title", "net3_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Okraj", "Kde končí vaše síť", "Tip: za routerem k poskytovateli už neřídíte fyzickou cestu.", + { + "WAN spojuje vzdálené sítě přes síť, kterou nevlastníte.", + "Poskytovatel vám dá adresu, bránu a často i VLAN.", + "Výchozí trasa obvykle míří právě sem.", + "Výpadek poslední míle odřízne pobočku, i když jádro žije.", + NULL, + }, + }, + { + "2 / 3 • Technologie", "Čím se to vozí", "Tip: dnešní WAN často vypadá jako ethernet, i když pod ním je jiná síť.", + { + "Starší spoje používaly pronajaté linky a PPP.", + "MPLS u poskytovatele spojí pobočky jako jednu službu.", + "Metro Ethernet přivede do budovy běžný ethernetový port.", + "Rychlost, odezva a ztráty jsou parametry, které se sledují.", + NULL, + }, + }, + { + "3 / 3 • Návrh", "Co domluvit", "Tip: záložní spoj má smysl jen s cestou, která se na něj umí přepnout.", + { + "Jedna přípojka je jednoduchá a křehká.", + "Druhý poskytovatel sníží riziko, když nejde stejnou trasou.", + "Adresy od poskytovatele musí sedět s vaším NAT a firewallem.", + "Na okraji filtrujte, co do školy z internetu lézt nemá.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 2, "net3_unit3", "net3_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je WAN z pohledu školy?", {"Spoj mimo budovu, často přes poskytovatele", "Kabel mezi dvěma porty jednoho switche", "VLAN pro tiskárny", "Diskové pole"}, 4, 0}, + {"Kam obvykle míří výchozí trasa?", {"K poskytovateli", "Na tiskárnu", "Do STP rootu", "Na adresu 127.0.0.1"}, 4, 0}, + {"Proč dva spoje stejnou ulicí nemusí být záloha?", {"Jedna porucha překope oba naráz", "OSPF dva spoje zakazuje", "IPv4 umí jen jednu bránu na světě", "Ethernet nesmí mít zálohu"}, 4, 0}, + {"Co je metro Ethernet?", {"WAN služba, která u vás končí ethernetovým portem", "Wi-Fi ve vlaku", "Protokol místo DNS", "Typ procesoru"}, 4, 0}, + }; + static const char *hints[] = { + "WAN překračuje místní síť.", + "Co směrovač nezná, pošle ven.", + "Fyzicky oddělená trasa je součást redundance.", + "Pod portem může být síť poskytovatele, vy vidíte ethernet.", + }; + return quizyear_quiz(qy_net3, 2, "net3_ex3_title", "net3_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tunel", "Cizí síť jako kabel", "Tip: VPN neschová provoz před sítí, ve které tunel končí.", + { + "Mezi dvěma body se postaví šifrovaný tunel.", + "Uvnitř mohou téct soukromé adresy.", + "Venku je vidět jen obal mezi konci tunelu.", + "Bez shodných klíčů nebo certifikátů tunel nevstane.", + NULL, + }, + }, + { + "2 / 3 • Dva tvary", "Pobočka a člověk", "Tip: site-to-site spojuje sítě, remote access jednoho uživatele.", + { + "Site-to-site propojí dvě pobočky trvale.", + "Remote access pustí notebook do školní sítě.", + "Uživatel se ověřuje účtem, ne jen společným heslem na zdi.", + "Split tunnel posílá tunelem jen školní sítě, zbytek jde přímo.", + NULL, + }, + }, + { + "3 / 3 • Chyby", "Když tunel stojí a data ne", "Tip: tunel může svítit zeleně a přesto chybí trasa dovnitř.", + { + "Musí sedět návrh šifry na obou koncích.", + "Firewall po cestě musí pustit port VPN.", + "Uvnitř musí existovat trasa do cílových sítí.", + "Příliš široký tunel stáhne na školu i provoz, který tam být neměl.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 3, "net3_unit4", "net3_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co VPN před cizí sítí skrývá?", {"Obsah a vnitřní adresy, venku je vidět obal tunelu", "I to, že nějaký provoz existuje", "MAC adresu switche ve třídě", "Úplně všechno včetně konců tunelu"}, 4, 0}, + {"Čím se liší site-to-site od remote access?", {"První spojuje sítě, druhá připojuje uživatele", "První je Wi-Fi, druhá je VLAN", "Obě jen mění DNS", "Remote access nejde šifrovat"}, 4, 0}, + {"Proč tunel svítí, ale server nehraje?", {"Chybí trasa nebo firewall uvnitř", "IPv4 zakazuje VPN", "STP tunely maže", "DNS přejmenuje šifru"}, 4, 0}, + {"Co je split tunnel?", {"Tunelem jde jen provoz do vnitřních sítí", "Každý paket se rozpůlí", "VPN bez šifry", "Dva SSID na jednom kanále"}, 4, 0}, + }; + static const char *hints[] = { + "Koncové adresy tunelu vidět jsou, náklad ne.", + "Notebook není druhá pobočka, i když tunel vypadá podobně.", + "Šifra a dosažitelnost jsou dvě vrstvy.", + "Internet z notebooku pak nemusí téct přes školu.", + }; + return quizyear_quiz(qy_net3, 3, "net3_ex4_title", "net3_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Zóny", "Důvěra není stejná", "Tip: LAN, WAN a DMZ nemají stejná pravidla.", + { + "Rozhraní se sdruží do zón podle důvěry.", + "Z důvěryhodnější zóny ven se často povoluje snáz.", + "Z internetu dovnitř se povoluje jen to, co je schválené.", + "Pravidlo se píše od zóny ke zóně, ne jen na jednu adresu.", + NULL, + }, + }, + { + "2 / 3 • Stav", "Pamatuje si spojení", "Tip: stavový firewall pustí odpověď, aniž byste psali pravidlo nazpátek.", + { + "Nové spojení se kontroluje proti politice.", + "Odpověď patřící ke spojení se pustí.", + "Bezestavový filtr zkoumá každý paket zvlášť.", + "Ukončené spojení se z tabulky smaže.", + NULL, + }, + }, + { + "3 / 3 • DMZ", "Server, který má být vidět", "Tip: web v DMZ nemá mít cestu ke všem diskům ve škole.", + { + "Veřejná služba sedí v oddělené zóně.", + "Z internetu se otevře jen její port.", + "Z DMZ do LAN se pustí jen to, co služba opravdu potřebuje.", + "Kompromitovaný web pak neotevře celou školu.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 4, "net3_unit5", "net3_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč se zóny liší?", {"Mají jinou důvěru a tedy jiná pravidla", "Každá zóna musí mít vlastní DNS jméno switche", "Zóny nahrazují IP adresy", "Bez zón nejde STP"}, 4, 0}, + {"Co umí stavový firewall?", {"Pustí odpověď k už otevřenému spojení", "Smaže VLAN", "Přidělí MAC adresu", "Nahradí směrovací protokol"}, 4, 0}, + {"Kam patří veřejný web, který nemá vidět celou LAN?", {"Do DMZ", "Do stejné sítě jako účtárna", "Na access port ředitele", "Do nativní VLAN trunku bez pravidel"}, 4, 0}, + {"Co samotná zóna ještě není?", {"Náhrada za konkrétní pravidla služeb", "Způsob, jak sdružit rozhraní", "Hranice důvěry", "Místo, kam se píše politika"}, 4, 0}, + }; + static const char *hints[] = { + "Internet a učebna nejsou stejně důvěryhodné.", + "Pamatuje si, kdo spojení začal.", + "DMZ omezí, kam se útočník dostane dál.", + "Zóna bez pravidel nic nechrání.", + }; + return quizyear_quiz(qy_net3, 4, "net3_ex5_title", "net3_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Fronty", "Ne všechno snese čekání", "Tip: QoS nevytvoří rychlost, jen rozhodne, kdo počká.", + { + "Hlas a videohovor kazí zpoždění a kolísání.", + "Záloha disku snese čekání líp než telefonát.", + "Fronta na úzké lince řadí pakety.", + "Bez označení provozu switch neví, co je hlas.", + NULL, + }, + }, + { + "2 / 3 • Značky", "Kdo si řekne o přednost", "Tip: značce z koncového portu nevěřte, uživatel si ji může nastavit sám.", + { + "IP hlavička umí nést DSCP.", + "Ethernet umí prioritu na VLAN značce.", + "Na vstupu se značka přepíše podle politiky.", + "Hlasová VLAN se často značí automaticky.", + NULL, + }, + }, + { + "3 / 3 • Praxe", "Kde to má smysl", "Tip: na prázdné gigabitové lince QoS nic viditelného neudělá.", + { + "Úzké hrdlo bývá WAN, ne páteřní switch.", + "Rezervace pro hlas nesmí sežrat celou linku.", + "Testujte hovor ve chvíli, kdy běží velká kopie.", + "Špatná politika umí zpomalit i správu sítě.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 5, "net3_unit6", "net3_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co QoS neumí?", {"Vyrobit větší rychlost linky", "Dát přednost hlasu před zálohou", "Omezit kolísání hovoru", "Označit provoz"}, 4, 0}, + {"Proč se značka z počítače přepisuje?", {"Uživatel by si jinak dal nejvyšší prioritu", "DSCP nejde číst", "IPv4 značky zakazuje", "STP značky maže"}, 4, 0}, + {"Kde QoS obvykle pomůže nejdřív?", {"Na pomalé WAN lince", "Na prázdném 10Gb portu mezi switchi", "Uvnitř jednoho počítače na smyčce", "Na vypnutém Wi-Fi"}, 4, 0}, + {"Který provoz čekání snese hůř?", {"Telefonní hovor", "Noční kopie disku", "Stahování aktualizace", "Archiv pošty"}, 4, 0}, + }; + static const char *hints[] = { + "Dělí existující kapacitu, nepřidá ji.", + "Důvěryhodná značka vznikne na vstupu podle pravidel.", + "Přednost se projeví, až když se linka plní.", + "Hlas potřebuje malé a stálé zpoždění.", + }; + return quizyear_quiz(qy_net3, 5, "net3_ex6_title", "net3_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • SNMP", "Čísla z zařízení", "Tip: SNMPv2c posílá jméno komunity skoro jako heslo v čistém textu.", + { + "Dohled se ptá na vytížení, chyby a stav portů.", + "Komunita není ozdoba, je to přístup.", + "SNMPv3 umí uživatele a šifrování.", + "Zařízení má poslouchat SNMP jen ze sítě dohledu.", + NULL, + }, + }, + { + "2 / 3 • Syslog", "Věty o tom, co se stalo", "Tip: log na zařízení se při restartu může ztratit. Posílejte ho pryč.", + { + "Syslog odnáší hlášky na server.", + "Úrovně od ladění po nouzi se nemají všechny sypat do jedné hromady.", + "Čas na zařízeních musí sedět, jinak nejde srovnat události.", + "NTP je proto součást dohledu, ne jen hodiny na zdi.", + NULL, + }, + }, + { + "3 / 3 • Čtení", "Graf není diagnóza", "Tip: špička jednou za den může být záloha, ne útok.", + { + "Chyby na portu často znamenají kabel nebo duplex.", + "Vytrvalé vytížení WAN řekne, jestli je čas na silnější spoj.", + "Výpadek, který nikdo nevidí, se bude opakovat.", + "Upozornění má chodit člověku, který může reagovat.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 6, "net3_unit7", "net3_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč SNMPv2c nestačí na nezabezpečené síti?", {"Jméno komunity jde odposlechnout", "Neumí číst vytížení", "Funguje jen na IPv6", "Maže syslog"}, 4, 0}, + {"Proč posílat syslog pryč ze zařízení?", {"Lokální log se při restartu může ztratit", "Syslog nahrazuje heslo", "Switch jinak vypne porty", "DNS bez logu nefunguje"}, 4, 0}, + {"K čemu je při dohledu NTP?", {"Aby časy událostí šly srovnat", "Aby přidělil VLAN", "Aby šifroval Wi-Fi", "Aby nahradil SNMP"}, 4, 0}, + {"Co často znamenají rostoucí chyby na portu?", {"Špatný kabel nebo duplex", "Že je VLAN správně", "Že OSPF konverguje", "Že DNS TTL vypršelo"}, 4, 0}, + }; + static const char *hints[] = { + "Komunita cestuje bez pořádné ochrany. Lepší je SNMPv3.", + "Po restartu už příčinu pádu na zařízení nemusíte najít.", + "Bez společného času nejde říct, co bylo dřív.", + "Fyzická vrstva se v počítadlech chyb projeví dřív než v teorii.", + }; + return quizyear_quiz(qy_net3, 6, "net3_ex7_title", "net3_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net3_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Běžící a uložená", "Restart není uložení", "Tip: změna v běžící konfiguraci po restartu zmizí, dokud ji neuložíte.", + { + "Běžící konfigurace je to, co zařízení právě dělá.", + "Startovní se načte po zapnutí.", + "Uložení zkopíruje běžící do startovní.", + "Bez uložení druhý den síť vypadá jako včera.", + NULL, + }, + }, + { + "2 / 3 • Kopie ven", "Nejen na zařízení", "Tip: záloha na tom samém přepínači shoří spolu s ním.", + { + "Konfigurace patří na server mimo to zařízení.", + "Ukládejte i verzi, ať víte, co se změnilo.", + "Hesla v exportu mohou být čitelná. Soubor chraňte.", + "Automatická záloha po změně porazí dobrý úmysl.", + NULL, + }, + }, + { + "3 / 3 • Popis", "Aby to přečetl i kolega", "Tip: port bez popisu je za půl roku hádanka.", + { + "Popis portu říká, co je na druhém konci.", + "Schéma VLAN a adres má sedět s konfigurací.", + "Změna bez poznámky se špatně vrací.", + "Nový switch postavíte z zálohy jen tehdy, když záloha existuje.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net3, 7, "net3_unit8", "net3_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net3_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co se stane se změnou, kterou neuložíte?", {"Po restartu zmizí", "Zapíše se do VLAN navždy", "Přepíše firmware", "Pošle se sama do DNS"}, 4, 0}, + {"Proč záloha jen na tom samém switchi nestačí?", {"S poškozeným switchem zmizí i ona", "Switch zálohy šifruje tak, že je nečitelné i pro vás", "STP zálohy maže", "IPv6 zálohy zakazuje"}, 4, 0}, + {"Co má být u portu napsané?", {"Co je na druhém konci", "Heslo administrátora", "Celá směrovací tabulka", "Soukromý klíč VPN"}, 4, 0}, + {"Proč chránit soubor se zálohou?", {"Může obsahovat čitelná hesla", "Soubor sám otevírá porty", "DNS ho publikuje", "Je větší než disk"}, 4, 0}, + }; + static const char *hints[] = { + "Běžící konfigurace není totéž co startovní.", + "Kopie musí žít jinde.", + "Popis portu šetří hodiny při poruše.", + "Export není vždy v zašifrované podobě.", + }; + return quizyear_quiz(qy_net3, 7, "net3_ex8_title", "net3_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Rozsahy", "Každá VLAN má svůj prefix", "Tip: nenechávejte v každé síti jen čtyři volné adresy.", + { + "Nejdřív spočítejte stanice, tiskárny, AP a rezervu.", + "Brána, síťová a broadcast adresa se do stanic nepočítají.", + "Sousední VLAN nemají mít překryv.", + "Dokument říká, která čísla jsou volná.", + NULL, + }, + }, + { + "2 / 3 • Členění", "Škola není jedna velká síť", "Tip: /16 na celou budovu se snadno spravuje a špatně izoluje.", + { + "Učebny, správa, servery, hosté a telefony patří odděleně.", + "Stejný řád čísel ve všech budovách se líp pamatuje.", + "Souhrn tras na páteři zmenší tabulku.", + "Výjimka pro jednu tiskárnu se má zapsat, ne jen zapamatovat.", + NULL, + }, + }, + { + "3 / 3 • Změna", "Plán, který jde rozšířit", "Tip: když přidáte budovu, neměli byste přečíslovat celou školu.", + { + "Nechte v páteřním rozsahu díry pro další sítě.", + "Přechod na nový prefix znamená DHCP, DNS, brány i firewall.", + "IPv6 prefix se plánuje stejně vědomě, ne jako dodatek.", + "Tabulka adres je součást maturity i běžného provozu.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 0, "net4_unit1", "net4_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co se v síti /24 nedá přiřadit stanici?", {"Adresa sítě a broadcast", "První použitelná adresa", "Adresa brány, pokud ji stanice nemá mít", "Adresa s lichým posledním oktetem"}, 4, 0}, + {"Proč nerozdat celé škole jednu velkou síť?", {"Špatně se izoluje porucha i provoz", "IPv4 to zakazuje", "Switch umí jen 8 adres", "DNS pak nejde spustit"}, 4, 0}, + {"K čemu je souhrn tras?", {"Jedna trasa pokryje víc menších sítí", "Smaže VLAN", "Nahradí NAT", "Zapne WPA3"}, 4, 0}, + {"Co patří do adresního plánu?", {"Prefix, VLAN, brána a k čemu síť je", "Jen heslo Wi-Fi", "Typ procesoru serveru", "Seznam učitelů"}, 4, 0}, + }; + static const char *hints[] = { + "Krajní adresy mají zvláštní význam. Brána se stanicím také nedává dvakrát.", + "Menší sítě se líp hlídají a míň se ruší broadcastem.", + "Páteř nemusí znát každou učebnu zvlášť.", + "Bez účelu sítě je tabulka jen sloupec čísel.", + }; + return quizyear_quiz(qy_net4, 0, "net4_ex1_title", "net4_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dvojice", "Dva boxy nestačí", "Tip: dva switche se stejnou konfigurací nejsou záloha, dokud provoz neumí přejít.", + { + "Redundance chce druhou cestu i protokol, který ji hlídá.", + "STP řeší smyčku na druhé vrstvě.", + "Na třetí vrstvě zálohu hlídá směrování nebo HSRP a příbuzné protokoly.", + "Jedna brána v DHCP znamená, že pád routeru odstřihne všechny.", + NULL, + }, + }, + { + "2 / 3 • Brána", "Virtuální adresa", "Tip: stanice mají jako bránu virtuální adresu, ne jednu fyzickou krabici.", + { + "Dva směrovače sdílejí jednu adresu brány.", + "Aktivní drží provoz, druhý čeká.", + "Když aktivní ztichne, druhý adresu převezme.", + "Stanice nemusí dostat nové DHCP.", + NULL, + }, + }, + { + "3 / 3 • Zkouška", "Nevyzkoušená záloha je teorie", "Tip: vytáhněte kabel v okně, kdy smíte, ne až při maturitní písemce.", + { + "Ověřte, že záložní cesta opravdu přenáší data.", + "Sledujte, za jak dlouho se provoz vrátí.", + "Záložní zdroj má jinou pojistku a jinou fázi, když to jde.", + "Zápis z zkoušky říká, co se má stát příště.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 1, "net4_unit2", "net4_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč dva stejné routery samy o sobě nestačí?", {"Stanice musí mít kam přejít, když jeden ztichne", "IPv4 povoluje jen jeden router v budově", "Druhý router maže VLAN", "STP dva routery zakazuje"}, 4, 0}, + {"Co stanice používá při protokolu virtuální brány?", {"Virtuální adresu, ne konkrétní krabici", "Svou vlastní MAC jako bránu", "Adresu DNS", "Broadcast 255.255.255.255 jako další skok"}, 4, 0}, + {"Kdy se záloha ověřuje?", {"Nanečisto, dřív než o ni opřete provoz", "Až když síť leží", "Jen na papíře", "Záloha se ověřovat nemá, ať se nepokazí"}, 4, 0}, + {"Co na druhé vrstvě řeší náhradní kabel?", {"STP, jinak vznikne smyčka", "Jen NAT", "Jen WPA3", "Nic, ethernet smyčky umí sám"}, 4, 0}, + }; + static const char *hints[] = { + "Bez společné nebo přepínané brány počítače zůstanou na mrtvé adrese.", + "Převzetí adresy stanice nepozná jako výměnu DHCP.", + "Nevyzkoušený přepínač překvapí v nejhorší chvíli.", + "Druhý kabel bez STP je recept na bouři.", + }; + return quizyear_quiz(qy_net4, 1, "net4_ex2_title", "net4_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Port", "Nejdřív průkaz", "Tip: dokud ověření neskončí, na portu nemá téct běžný provoz.", + { + "Počítač se prokáže certifikátem nebo jménem a heslem.", + "Switch se ptá serveru RADIUS.", + "Až pak pustí port do dané VLAN.", + "Neznámé zařízení zůstane venku nebo v omezené síti.", + NULL, + }, + }, + { + "2 / 3 • Lidé a věci", "Ne všechno umí 802.1X", "Tip: tiskárna bez klienta potřebuje výjimku, ne vypnutí celé ochrany.", + { + "Počítač v doméně zvládne ověření sám.", + "Tiskárna, kamera a telefon často ne.", + "MAB pustí zařízení podle MAC adresy. Tu jde zfalšovat.", + "Výjimka má být na konkrétní port, ne na celý switch.", + NULL, + }, + }, + { + "3 / 3 • VLAN", "Kam koho pustit", "Tip: učitel a host nemají po ověření skončit ve stejné VLAN.", + { + "Úspěch může přiřadit VLAN podle skupiny.", + "Neúspěch skončí v síti bez přístupu k serverům.", + "Účet jde odebrat, aniž by se měnilo heslo Wi-Fi.", + "Stejný princip sedí na kabel i na podnikovou Wi-Fi.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 2, "net4_unit3", "net4_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo rozhodne, že zařízení smí do sítě?", {"RADIUS na základě údajů od switche", "Sám kabel podle barvy", "DNS záznam A", "STP root"}, 4, 0}, + {"Proč MAB není silné ověření?", {"MAC adresu lze zfalšovat", "MAC adresa je jednorázové heslo", "MAB šifruje silněji než certifikát", "Funguje jen na IPv6"}, 4, 0}, + {"Kam patří zařízení, které ověřením neprojde?", {"Do omezené sítě nebo nikam", "Do VLAN serverů", "Na trunk všech VLAN", "Do správy routeru"}, 4, 0}, + {"Jaká je výhoda účtů proti jednomu heslu Wi-Fi?", {"Jednomu člověku jde přístup odebrat", "Heslo se nemusí nikdy měnit, protože účty ho ruší", "Netřeba RADIUS ani switch", "WEP pak stačí"}, 4, 0}, + }; + static const char *hints[] = { + "Switch se ptá, server odpovídá.", + "Adresa na štítku není průkaz.", + "Neúspěch nemá otevírat vnitřní síť.", + "Sdílené heslo po odchodu člověka zná pořád.", + }; + return quizyear_quiz(qy_net4, 2, "net4_ex3_title", "net4_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Vrstvy", "Nejdřív světlo na portu", "Tip: nehledejte chybu v DNS, když port nesvítí.", + { + "Fyzická vrstva: kabel, port, napájení, duplex, chyby.", + "Druhá vrstva: VLAN, MAC tabulka, trunk, STP.", + "Třetí vrstva: adresa, maska, brána, trasa, ACL.", + "Výš: DNS, port služby, firewall na konci.", + NULL, + }, + }, + { + "2 / 3 • Jedna změna", "Ať víte, co pomohlo", "Tip: pět změn naráz nic nenaučí.", + { + "Zapište, co vidíte, než něco přepnete.", + "Ověřte jednu hypotézu.", + "Ping na bránu, pak za ni, pak jméno.", + "Když oprava sedí, uložte konfiguraci a dopište důvod.", + NULL, + }, + }, + { + "3 / 3 • Rozsah", "Koho to postihlo", "Tip: jeden počítač je jiná chyba než celá učebna.", + { + "Jeden stroj: jeho kabel, port, adresa, účet.", + "Celá VLAN: brána, DHCP, trunk, SVI.", + "Celá škola: jádro, firewall, poskytovatel.", + "Čas začátku často sedí se změnou, kterou někdo neoznámil.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 3, "net4_unit4", "net4_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co ověříte jako první, když port nesvítí?", {"Kabel, port a napájení", "Záznam AAAA", "Cenu OSPF", "Pravidlo QoS pro hlas"}, 4, 0}, + {"Proč měnit jen jednu věc?", {"Ať je vidět, která změna pomohla", "Switch víc změn zakazuje", "IPv4 umí jen jednu opravu denně", "DNS jinak smaže zónu"}, 4, 0}, + {"Celá učebna nedostane adresu. Kde začnete?", {"DHCP, VLAN a brána té sítě", "Heslem jednoho studenta k webu", "Jasem monitoru", "Verzí prohlížeče"}, 4, 0}, + {"Co uděláte, když oprava sedí?", {"Uložíte konfiguraci a zapíšete důvod", "Restartujete všechno ještě jednou", "Smažete popis portů", "Vypnete dohled, ať neruší"}, 4, 0}, + }; + static const char *hints[] = { + "Vyšší vrstvy nemají na čem jet.", + "Více zásahů naráz zamaskuje příčinu.", + "Společný příznak ukazuje na společnou službu.", + "Neuložená oprava do rána zmizí.", + }; + return quizyear_quiz(qy_net4, 3, "net4_ex4_title", "net4_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Kam s tím", "Vlastní sál, nebo cizí", "Tip: cloud neruší síť ve škole, mění jen to, kam vede spoj.", + { + "Veřejný cloud provozuje poskytovatel a vy si berete službu.", + "Soukromý cloud je pořád vaše zařízení, jen obalené portálem.", + "Hybrid nechá část dat ve škole a část venku.", + "Účet a síťová cesta jsou pořád vaše odpovědnost.", + NULL, + }, + }, + { + "2 / 3 • Cesta", "Jak se tam školní síť dostane", "Tip: služba v cloudu bez cesty a bez jmen je jen faktura.", + { + "VPN do cloudu vypadá jako další pobočka.", + "Veřejná služba jede přes internet a firewall.", + "DNS musí ukazovat na nové adresy.", + "Výpadek WAN teď shodí i to, co dřív jelo z místního serveru.", + NULL, + }, + }, + { + "3 / 3 • Hranice", "Kdo zálohuje a kdo pustí dovnitř", "Tip: poskytovatel nezálohuje vaše data, dokud to není ve smlouvě.", + { + "Pravidla firewallu se stěhují s službou.", + "Účty v cloudu mají mít stejný řád jako účty ve škole.", + "Test obnovy platí i pro data, která nejsou ve vašem racku.", + "Škola pořád řeší osobní údaje, i když disk je jinde.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 4, "net4_unit5", "net4_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co hybridní síť znamená?", {"Část služeb je ve škole a část u poskytovatele", "Dva switche v jedné VLAN", "IPv4 i papírový sešit", "Wi-Fi bez hesla"}, 4, 0}, + {"Co se pokazí, když WAN spadne a pošta je jen v cloudu?", {"Škola se k poště nedostane", "Místní switch přestane dělat VLAN", "STP zvolí cloud jako root", "Nic, cloud WAN nepotřebuje"}, 4, 0}, + {"Kdo zálohuje data v cloudu?", {"Ten, koho to smlouva a nastavení opravdu určují", "Vždy automaticky stát", "STP", "Nikdo, cloud zálohy zakazuje"}, 4, 0}, + {"Co škola řeší i u cizího disku?", {"Kdo má přístup a jak se chrání osobní údaje", "Jen barvu loga", "Jen typ ventilátoru v sále poskytovatele", "Nic, odpovědnost přechází celá"}, 4, 0}, + }; + static const char *hints[] = { + "Hranice školy už není hranicí všech služeb.", + "Cesta ven je součást dostupnosti služby.", + "Předpoklad, že poskytovatel zálohuje vše, často neplatí.", + "Umístění dat nemaže pravidla přístupu.", + }; + return quizyear_quiz(qy_net4, 4, "net4_ex5_title", "net4_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • SSH", "Správa bez společného hesla na lístečku", "Tip: Telnet posílá heslo čitelně. Na správu patří SSH.", + { + "Klíč jde odebrat konkrétnímu člověku.", + "Heslo v příkazu historie nemá ležet.", + "Přístup omezte na správní síť.", + "Záznam, kdo se přihlásil, patří do logu.", + NULL, + }, + }, + { + "2 / 3 • Opakování", "Stejná VLAN ve dvaceti switchích", "Tip: ruční opis se v desátém switchi liší v jednom čísle.", + { + "Šablona drží názvy VLAN a popis portů.", + "Skript změnu pošle na seznam zařízení.", + "Nejdřív ho pusťte na jednom switchi.", + "Záloha před hromadnou změnou je povinnost, ne ozdoba.", + NULL, + }, + }, + { + "3 / 3 • Mez", "Skript není výmluva", "Tip: automat neopraví špatný záměr, jen ho provede rychleji.", + { + "Kontrola po běhu srovná, co opravdu sedí.", + "Tajné heslo nepatří do veřejného souboru.", + "Člověk pořád schvaluje změnu, která shodí výuku.", + "Verzovaný soubor konfigurace ukáže, kdo řádek změnil.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 5, "net4_unit6", "net4_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč na správu nepoužívat Telnet?", {"Heslo jde po síti čitelně", "Telnet neumí IPv4", "Switch Telnet zakazuje standardem STP", "Telnet maže VLAN"}, 4, 0}, + {"Jak pustíte stejnou změnu do dvaceti switchů?", {"Šablonou a skriptem, neopisovat ji dvacetkrát", "Jen restartem všech naráz", "Přes DHCP", "Jedním broadcastem bez kontroly"}, 4, 0}, + {"Co před hromadnou změnou uděláte?", {"Zálohu konfigurací", "Vypnete dohled", "Smažete popisy portů", "Zakážete SSH"}, 4, 0}, + {"Co skript neumí?", {"Poznat, že záměr byl špatně", "Poslat stejný příkaz víckrát", "Uložit konfiguraci", "Připojit se přes SSH"}, 4, 0}, + }; + static const char *hints[] = { + "SSH spojení chrání.", + "Nejdřív jeden switch, pak ostatní.", + "Ať se máte kam vrátit.", + "Rychlost neopraví chybný příkaz.", + }; + return quizyear_quiz(qy_net4, 5, "net4_ex6_title", "net4_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Řád", "Kdo smí co změnit", "Tip: heslo admin na lístečku u switche není provozní řád.", + { + "Účty jsou jmenné, ne jedno společné admin.", + "Změna ve výuce se hlásí dopředu.", + "Hostovská síť je oddělená a časově omezená.", + "Osobní údaje v logu se nenechávají ležet bez důvodu.", + NULL, + }, + }, + { + "2 / 3 • Výpadek", "Co říct a co udělat", "Tip: nejdřív rozsah a dopad, pak ladění detailu.", + { + "Řekněte, co nejede a koho se to týká.", + "Oddělte výpadek poskytovatele od chyby ve škole.", + "Po obnově ověřte poštu, přihlášení i tisk, ne jen ping.", + "Krátký zápis příště zkrátí stejnou poruchu.", + NULL, + }, + }, + { + "3 / 3 • U zkoušky", "Co umět ukázat", "Tip: maturita chce důvod, ne jen zkratku.", + { + "Umíte přečíst adresu, masku, bránu a VLAN.", + "Umíte říct, k čemu je trunk, NAT, firewall a záloha.", + "Schéma malé sítě nakreslíte i bez nápovědy.", + "Postup hledání chyby od kabelu po službu odvykládáte po vrstvách.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 6, "net4_unit7", "net4_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jak nemají vypadat účty ke switchi?", {"Jedno heslo admin pro celou sborovnu", "Jmenný účet s možností odebrání", "Přístup jen ze správní sítě", "Záznam přihlášení"}, 4, 0}, + {"Co řeknete jako první při výpadku?", {"Co nejede a koho se to týká", "Celou směrovací tabulku zpaměti", "Typ ventilátoru v racku", "Že se to vyřeší samo"}, 4, 0}, + {"Co po obnově nestačí?", {"Jen ping na bránu", "Ověřit poštu a přihlášení", "Podívat se na tisk", "Zkontrolovat, že VLAN sedí"}, 4, 0}, + {"Co u maturity k VLAN umíte říct?", {"Odděluje provoz a mezi VLAN se směruje", "Je to typ pevného disku", "Nahrazuje zálohu konfigurace", "Je totéž co WPA3"}, 4, 0}, + }; + static const char *hints[] = { + "Společné heslo nejde vzít jednomu člověku.", + "Rozsah je důležitější než první domněnka.", + "Brána může žít a služba pořád ne.", + "Definice a důvod, ne jen zkratka.", + }; + return quizyear_quiz(qy_net4, 6, "net4_ex7_title", "net4_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_net4_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Schéma", "Co na něm má být", "Tip: schéma z doby instalace bez datumu je pohádka.", + { + "Jsou na něm jádro, učebny, VLAN a přípojka ven.", + "Adresní plán sedí se schématem, ne s pamětí.", + "Porty páteře mají popisky na obou koncích.", + "Datum a jméno autora říkají, jak je obrázek starý.", + NULL, + }, + }, + { + "2 / 3 • Změna", "Nejdřív papír, pak kabel", "Tip: přepojit a zapsat až za měsíc znamená, že zápis už nesedí.", + { + "Změna VLAN se zapíše ve stejný den.", + "Fotka rozvaděče doplní schéma tam, kde čáry lžou.", + "Hesla do schématu nepatří. Patří do trezoru účtů.", + "Kopie schématu není jen na počítači, který ta síť živí.", + NULL, + }, + }, + { + "3 / 3 • Předání", "Až tu nebudete", "Tip: kolega má síť převzít bez vašeho telefonu.", + { + "Seznam zařízení, adres správy a kde leží zálohy.", + "Kontakt na poskytovatele a číslo spoje.", + "Co se nesmí restartovat ve výuce.", + "Jedna stránka je lepší než nikde neuložená hlava.", + NULL, + }, + }, + }; + return quizyear_unit(qy_net4, 7, "net4_unit8", "net4_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_net4_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co do schématu sítě nepatří?", {"Heslo správce", "VLAN a jejich účel", "Přípojka k poskytovateli", "Datum, kdy naposledy sedělo"}, 4, 0}, + {"Kdy se změna zapíše?", {"V den, kdy se provede", "Až se na ni někdo zeptá u maturity", "Jen když spadne celá škola", "Zápis není potřeba, stačí paměť"}, 4, 0}, + {"Proč mít kopii schématu i mimo školní server?", {"Když síť neběží, schéma na ní neotevřete", "Server schémata maže", "IPv6 schémata zakazuje", "STP obrázky nepouští"}, 4, 0}, + {"Co má kolega najít bez vašeho telefonu?", {"Adresy správy, zálohy a kontakt na poskytovatele", "Jen barvu racku", "Vaše soukromé heslo", "Nic, síť se nepředává"}, 4, 0}, + }; + static const char *hints[] = { + "Heslo patří jinam než na obrázek, který visí ve sborovně.", + "Nezapsaná změna je příští závada.", + "Dokumentace má přežít výpadek toho, co popisuje.", + "Provoz nesmí stát na jednom člověku.", + }; + return quizyear_quiz(qy_net4, 7, "net4_ex8_title", "net4_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Panel", "LCD a OLED", "Tip: rozbité podsvícení LCD zhasne obraz, i když panel pořád kreslí.", + { + "LCD svítí podsvícením a panel světlo cloní.", + "OLED svítí samotnými body a umí opravdovou černou.", + "Lesklý panel má vyšší kontrast a víc odlesků.", + "Uhlopříčka v palcích není totéž co pracovní plocha v bodech.", + NULL, + }, + }, + { + "2 / 3 • Obraz", "Rozlišení a frekvence", "Tip: nativní rozlišení panelu je to, na které je mřížka bodů.", + { + "Nižší rozlišení interpoluje a obraz měkne.", + "Obnovovací frekvence říká, kolikrát za sekundu se obraz překreslí.", + "Odezva panelu není totéž co frekvence.", + "HDR chce vyšší jas, ne jen zaškrtnutou volbu.", + NULL, + }, + }, + { + "3 / 3 • Kabel", "Obraz musí stihnout pásmo", "Tip: starý kabel nebo slabá verze HDMI oseká frekvenci i rozlišení.", + { + "DisplayPort a HDMI mají verze s různým datovým tokem.", + "VGA je analogové a na dlouhém kabelu měkne.", + "USB-C obraz poveze jen tehdy, když port umí alternativní režim.", + "Dokovací stanice má vlastní limit, ne limit notebooku.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 0, "hw2_unit1", "hw2_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se OLED liší od běžného LCD?", {"Body samy svítí a černá je opravdu zhasnutá", "Nemá rozlišení", "Jede jen po VGA", "Nepotřebuje grafickou kartu"}, 4, 0}, + {"Co je nativní rozlišení?", {"Počet bodů, na který je panel vyroben", "Nejvyšší číslo v herním menu bez ohledu na panel", "Rychlost procesoru", "Počet USB portů"}, 4, 0}, + {"Proč 4K při 144 Hz někdy nejde?", {"Kabel nebo verze portu nestihne datový tok", "OLED zakazuje frekvenci nad 60 Hz", "IPv4 omezuje monitory", "BIOS maže rozlišení"}, 4, 0}, + {"Kdy USB-C nepřenese obraz?", {"Když port umí jen data a nabíjení", "Když je kabel černý", "Když má notebook SSD", "Když je v síti VLAN"}, 4, 0}, + }; + static const char *hints[] = { + "LCD obraz skládá cloněním podsvícení.", + "Jiné rozlišení se musí přepočítat.", + "Pásmo portu je strop, ne přání v menu.", + "Konektor stejného tvaru ještě neznamená DisplayPort v něm.", + }; + return quizyear_quiz(qy_hw2, 0, "hw2_ex1_title", "hw2_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Princip", "Toner a kapky", "Tip: laser netaví toner na papír sám, dělá to zapékací válec.", + { + "Laser nabije válec a toner se chytí podle obrazu.", + "Inkoust stříká kapky a hodí se na fotky i občasný tisk.", + "Zaschnutá hlava je častější než porucha desky.", + "Termotransfer na štítky není totéž co kancelářský laser.", + NULL, + }, + }, + { + "2 / 3 • Síť", "Jedna tiskárna pro učebnu", "Tip: tiskárna s vlastní adresou má rezervaci v DHCP.", + { + "Sdílená tiskárna na počítači žije, jen když je ten počítač zapnutý.", + "Síťová tiskárna poslouchá sama.", + "Fronta na serveru drží ovladač na jednom místě.", + "Špatný ovladač tiskne znaky místo stránky.", + NULL, + }, + }, + { + "3 / 3 • Sken", "Obraz zpět do počítače", "Tip: rozlišení 600 dpi na text stačí, plakát chce víc.", + { + "Plochý skener i podavač mají jiný limit gramáže.", + "ADF nezvládne sešité listy.", + "Síťový sken může poslat soubor do složky nebo na poštu.", + "Sklo a podavač se mažou, jinak jsou na stránce pruhy.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 1, "hw2_unit2", "hw2_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co v laserové tiskárně připevní toner k papíru?", {"Zapékací válec", "Inkoustová hlava", "Ventilátor zdroje", "VGA kabel"}, 4, 0}, + {"Kdy sdílená tiskárna na učitelském počítači netiskne?", {"Když je ten počítač vypnutý", "Když má tiskárna vlastní IP a server", "Když je v síti DHCP", "Když je papír v zásobníku"}, 4, 0}, + {"Proč dát tiskárně rezervaci v DHCP?", {"Aby ovladače mířily pořád na stejnou adresu", "Aby se zvýšilo rozlišení", "Aby toner vydržel dýl", "Aby nešla skenovat"}, 4, 0}, + {"Co podavač skeneru nezvládne?", {"Sešitou sadu listů", "Jednotlivé běžné listy", "Černobílý text", "Odeslání do složky, pokud to umí firmware"}, 4, 0}, + }; + static const char *hints[] = { + "Teplo a tlak toner zataví.", + "Sdílení přes počítač z toho počítače dělá server.", + "Nová adresa po restartu rozbije frontu.", + "Sešit patří na sklo, ne do ADF.", + }; + return quizyear_quiz(qy_hw2, 1, "hw2_ex2_title", "hw2_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • USB", "Versie a napájení", "Tip: port USB-A 2.0 fyzicky vezme i rychlý flash disk a stáhne ho na svou rychlost.", + { + "USB 2.0, 3.x a 4 se liší tokem dat.", + "USB-C je tvar, ne záruka rychlosti.", + "Thunderbolt po stejném tvaru umí obraz, síť i disk.", + "Hub bez napájení nestačí pro víc disků.", + NULL, + }, + }, + { + "2 / 3 • SATA", "Klasické disky", "Tip: napájení SATA a datový kabel jsou dva různé vodiče.", + { + "HDD i starší SSD sedí na SATA.", + "Rychlost rozhraní je strop, disk může být pomalejší.", + "Kabel bez západky vypadne a disk zmizí z BIOSu.", + "Externí box schová SATA za USB.", + NULL, + }, + }, + { + "3 / 3 • NVMe", "SSD přímo na PCIe", "Tip: M.2 je rozměr destičky. NVMe a SATA na M.2 nejsou totéž.", + { + "NVMe jede po PCIe a obchází starý řadič SATA.", + "Slot M.2 na desce může umět jen jednu z variant.", + "Dlouhá destička do krátkého slotu nepatří.", + "Bez chlazení rychlé SSD ve špičce zpomalí.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 2, "hw2_unit3", "hw2_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co říká samotný tvar USB-C?", {"Jen tvar konektoru, ne rychlost ani obraz", "Vždy Thunderbolt", "Vždy 40 Gb/s", "Vždy nabíjení 100 W a DisplayPort"}, 4, 0}, + {"Čím se NVMe liší od SATA SSD?", {"Jede po PCIe, ne po starém SATA", "Nemá paměťové buňky", "Jde zapojit jen do VGA", "Nepotřebuje desku"}, 4, 0}, + {"Co může umět slot M.2?", {"Jen SATA, jen NVMe, nebo obojí, podle desky", "Vždy obojí", "Jen grafickou kartu", "Jen operační paměť"}, 4, 0}, + {"Proč rychlé SSD ve špičce zpomalí?", {"Přehřeje se a stáhne výkon", "SATA mu sebere VLAN", "BIOS maže buňky", "USB-C ho přepne na 2.0 vždy"}, 4, 0}, + }; + static const char *hints[] = { + "Schopnosti určuje řadič a kabel, ne obrys.", + "Protokol a sběrnice jsou rychlejší cesta.", + "Manuál desky je závaznější než tvar zářezu.", + "Řadič hlídá teplotu.", + }; + return quizyear_quiz(qy_hw2, 2, "hw2_ex3_title", "hw2_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tok", "Podtlak a přetlak", "Tip: všechny ventilátory foukající dovnitř bez odtahu jen víří prach.", + { + "Vpředu a dole se vzduch nasává, vzadu a nahoře odchází.", + "Filtr patří na vstup, ne na výfuk.", + "Zavřená přední stěna designové skříně škrtí přívod.", + "Kabel přes ventilátor je ucpaný filtr.", + NULL, + }, + }, + { + "2 / 3 • CPU", "Chladič musí sednout", "Tip: pasta není vrstva másla. Má vyplnit nerovnosti.", + { + "Vzduchový chladič potřebuje místo k bočnici.", + "Voda odvede teplo do radiátoru, který pořád potřebuje vzduch.", + "Špatně dotažený chladič pustí teplotu hned po startu.", + "Limit teploty v BIOSu je pojistka, ne pracovní režim.", + NULL, + }, + }, + { + "3 / 3 • Prach", "Tichý vrah zdroje", "Tip: vrstva prachu na žebru je izolace.", + { + "Zdroj s filtrem se čistí, nezalepuje.", + "Notebookový výfuk do měkké deky se udusí.", + "Po roce bez čištění rostou otáčky a teplota.", + "Čištění stlačeným vzduchem nedrží ventilátor v roztočeném stavu naslepo.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 3, "hw2_unit4", "hw2_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kam patří prachový filtr?", {"Na vstup vzduchu", "Jen na výfuk vzadu", "Dovnitř zdroje bez mřížky", "Na displej"}, 4, 0}, + {"Co dělá příliš silná vrstva pasty?", {"Zhorší přestup tepla", "Zvýší takt samo", "Nahradí chladič", "Opraví špatný socket"}, 4, 0}, + {"Proč vodní chlazení pořád potřebuje vzduch?", {"Radiátor musí teplo předat do místnosti", "Voda chladí jen při otevřeném okně skříně", "Čerpadlo jede na VGA", "Bez ventilátoru se voda vaří vždy do minuty"}, 4, 0}, + {"Co udělá notebook na dece?", {"Zakryje sání a přehřeje se", "Zvýší jas panelu", "Přepne disk na NVMe", "Opraví ventilátor"}, 4, 0}, + }; + static const char *hints[] = { + "Filtruje se to, co teprve vstupuje.", + "Pasta vede hůř než kov chladiče.", + "Okruh teplo přesune, radiátor ho odvede.", + "Sání bývá zespodu.", + }; + return quizyear_quiz(qy_hw2, 3, "hw2_ex4_title", "hw2_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Příprava", "Nejdřív mimo skříň", "Tip: sestavu nejdřív oživte na stole. Ve skříni se špatně hledá nedosedlá paměť.", + { + "Deska, procesor, chladič a jeden modul paměti.", + "Zdroj mimo skříň a obraz na základní grafiku.", + "Až potom disk, karty a zbytek paměti.", + "Antistatická podložka a klidné ruce porazí koberec.", + NULL, + }, + }, + { + "2 / 3 • Socket", "Procesor nemá kam spadnout", "Tip: ohnutý pin na desce LGA nevrátíte pinzetou naslepo.", + { + "Trojúhelník na procesoru sedí se značkou na socketu.", + "Páka se zavírá až když procesor leží rovně.", + "Pasta až po usazení, ne do socketu.", + "Chladič se dotahuje křížem, ne jednou stranou nadoraz.", + NULL, + }, + }, + { + "3 / 3 • Skříň", "Kabely jsou součást airflow", "Tip: distanční sloupky pod deskou musí sedět s otvory, jinak deska zkratuje.", + { + "Nejdřív zdroj a distanční sloupky, potom deska.", + "Napájení desky, procesoru a karty jsou tři různé konektory.", + "Přední USB a tlačítko mají malé konektory podle manuálu.", + "První start se sleduje: ventilátory, obraz, pípnutí nebo LED.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 4, "hw2_unit5", "hw2_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč oživit sestavu nejdřív mimo skříň?", {"Nedosedlá paměť se ve skříni hledá hůř", "Ve skříni nejde zapnout zdroj", "BIOS ve skříni neběží", "Pasta venku schne"}, 4, 0}, + {"Co se nesmí stát pinům na socketu LGA?", {"Ohnout je", "Nechat je v socketu", "Srovnat procesor podle značky", "Zavřít páku až po usazení"}, 4, 0}, + {"K čemu jsou distanční sloupky?", {"Drží desku dál od plechu skříně", "Napájí ventilátor", "Jsou to šrouby do disku", "Uzemňují jen přední LED"}, 4, 0}, + {"Který konektor nepatří do napájení procesoru?", {"PCIe konektor grafické karty místo EPS", "Hlavní 24pin na desku", "EPS konektor k socketu", "SATA napájení disku"}, 4, 0}, + }; + static const char *hints[] = { + "Minimum dílů na stole ukáže, jestli jádro žije.", + "Pin na desce je křehký.", + "Bez nich nebo se špatnými sloupky deska zkratuje.", + "Konektory si jsou podobné a sedí špatně záměrně.", + }; + return quizyear_quiz(qy_hw2, 4, "hw2_ex5_title", "hw2_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Firmware", "BIOS a UEFI", "Tip: UEFI není jen barevné menu. Umí jiné dělení disku a Secure Boot.", + { + "Firmware otestuje železo a najde disk se systémem.", + "UEFI používá tabulku GPT, starý BIOS často MBR.", + "Secure Boot pustí jen podepsaný zavaděč.", + "Vypnutí Secure Bootu je výjimka, ne výchozí rada.", + NULL, + }, + }, + { + "2 / 3 • Pořadí", "Z čeho se startuje", "Tip: flash disk v pořadí před systémem nabootuje omylem instalaci.", + { + "Nabídka bootu umí jednorázově vybrat zařízení.", + "Disk bez zavaděče se v seznamu nechytí.", + "Více systémů potřebuje bootovací nabídku, ne přepisování pořád dokola.", + "Síťový boot PXE hledá obraz na serveru.", + NULL, + }, + }, + { + "3 / 3 • Heslo", "Menu není pro celou třídu", "Tip: heslo BIOSu bez záznamu je past, až se bude měnit disk.", + { + "Heslo na setup brání přehazování bootu.", + "Heslo na disk je silnější a bez něj jsou data nedostupná.", + "Clear CMOS heslo setupu často smaže a vezme i nastavení.", + "Po změně profilu paměti ověřte, že systém naběhne dvakrát.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 5, "hw2_unit6", "hw2_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co Secure Boot hlídá?", {"Aby se spustil jen důvěryhodný zavaděč", "Aby měl disk aspoň 1 TB", "Aby ventilátor točil na maximum", "Aby nešel USB flash disk vůbec zapojit"}, 4, 0}, + {"Čím se liší GPT od MBR?", {"GPT je tabulka oddílů pro UEFI a větší disky", "GPT je typ pasty", "MBR je jen pro NVMe", "Jsou to totéž"}, 4, 0}, + {"Proč nenechat flash disk trvale první v bootu?", {"Omylem z něj naběhne cizí systém", "Flash disk vypne Secure Boot navždy", "UEFI flash disky zakazuje", "Smaže TPM"}, 4, 0}, + {"Co často udělá vymazání CMOS?", {"Smaže heslo setupu i ostatní volby", "Smaže jen soubory na ploše", "Přepne Windows na jiný jazyk", "Opraví ohnutý pin"}, 4, 0}, + }; + static const char *hints[] = { + "Nepodepsaný zavaděč se nespustí.", + "Starý MBR má omezení, na která velký disk naráží.", + "Jednorázová nabídka bootu je bezpečnější.", + "Je to návrat firmware k výchozímu stavu.", + }; + return quizyear_quiz(qy_hw2, 5, "hw2_ex6_title", "hw2_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • POST", "Test hned po zapnutí", "Tip: žádný obraz a žádné pípnutí neznamená automaticky mrtvý procesor.", + { + "Firmware zkouší procesor, paměť a grafiku.", + "Kódy na displeji desky jsou přesnější než staré pípání.", + "Reproduktor skříně musí být zapojený, jinak ticho nic neřekne.", + "Manuál desky překládá kódy. Nejsou u všech stejné.", + NULL, + }, + }, + { + "2 / 3 • Minimum", "Uberte, co může lhát", "Tip: jeden modul paměti v doporučeném slotu porazí čtyři najednou.", + { + "Ven grafika, pokud má procesor vlastní obraz.", + "Ven disky a karty.", + "Zkuste jiný kabel obrazu a jiný port.", + "Až jádro naběhne, vracejte díly po jednom.", + NULL, + }, + }, + { + "3 / 3 • Paměť a disk", "Náhodné pády", "Tip: pád až ve Windows může být paměť, ne jen ovladač.", + { + "Test paměti běží déle než jedno pípnutí.", + "Chyby čtení disku se projeví v jeho vlastních statistikách.", + "Přehřátí shodí stroj až při zátěži, ne v menu BIOSu.", + "Vyměněný díl ověřte stejnou zkouškou, která předtím selhala.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 6, "hw2_unit7", "hw2_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co POST dělá?", {"Po zapnutí zkouší základní díly", "Instaluje systém", "Formátuje SSD", "Stahuje ovladače z internetu"}, 4, 0}, + {"Proč začít s jedním modulem paměti?", {"Špatný modul nebo slot se snáz odhalí", "Deska víc modulů zakazuje", "BIOS umí jen 4 GB", "Druhý modul maže UEFI"}, 4, 0}, + {"Kdy se přehřátí často neprojeví?", {"V menu BIOSu bez zátěže", "Při dlouhém testu procesoru", "Ve hře", "Při kódování videa"}, 4, 0}, + {"Co ověří výměnu dílu?", {"Stejná zkouška, která předtím selhala", "Jen to, že ventilátor se točí", "Nová tapeta", "Jiná barva skříně"}, 4, 0}, + }; + static const char *hints[] = { + "Je to první test firmware, ne operační systém.", + "Minimum dílů zužuje chybu.", + "Teplota roste až s odběrem.", + "Bez opakování testu nevíte, jestli to byla příčina.", + }; + return quizyear_quiz(qy_hw2, 6, "hw2_ex7_title", "hw2_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw2_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dok", "Jeden kabel, mnoho omezení", "Tip: levný USB hub není dok. Nepřenese obraz, který port neumí.", + { + "Dok může nést obraz, síť i napájení.", + "Limit je nejslabší článek: port, kabel nebo dok.", + "Dva externí monitory chtějí port, který to umí, ne přání uživatele.", + "Napájení přes dok nemusí stačit hernímu notebooku.", + NULL, + }, + }, + { + "2 / 3 • Síť", "Kabel z notebooku", "Tip: mnoho tenkých notebooků už ethernet nemá a potřebuje adaptér.", + { + "USB adaptér je síťová karta mimo šasi.", + "Rychlost adaptéru bývá 1 Gb/s, ne rychlost školní páteře.", + "Dok s vlastní síťovkou má vlastní MAC adresu.", + "Rezervace DHCP na MAC notebooku pak neplatí pro dok.", + NULL, + }, + }, + { + "3 / 3 • Výběr", "Na co se ptát", "Tip: stejný USB-C na dvou noteboocích nemá stejné schopnosti.", + { + "Kolik obrazů, jaké rozlišení a jestli se notebook nabíjí.", + "Jestli síť jede z doku, nebo se čeká Wi-Fi.", + "Jestli čtečka karet a USB sedí verzi, kterou škola používá.", + "Manuál portu je důležitější než fotka konektoru.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw2, 7, "hw2_unit8", "hw2_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw2_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdy USB-C dok nepřenese dva monitory?", {"Když port notebooku umí jen data", "Když je dok černý", "Když je v učebně VLAN", "Když má disk NVMe"}, 4, 0}, + {"Proč DHCP rezervace na notebook neplatí přes dok?", {"Dok má vlastní MAC adresu", "DHCP doky zakazuje", "VLAN maže rezervace", "Ethernet nemá MAC"}, 4, 0}, + {"Co je nejslabší článek řetězu obrazu?", {"Port, kabel nebo dok, podle toho, co umí nejmíň", "Vždy monitor", "Vždy procesor", "Vždy tapeta"}, 4, 0}, + {"Čím se liší USB hub od doku s obrazem?", {"Hub sám o sobě obraz z datového portu nevyrobí", "Hub je vždy rychlejší", "Dok neumí napájení", "Jsou to totéž"}, 4, 0}, + }; + static const char *hints[] = { + "Dok neumí vyrobit schopnost, kterou port nemá.", + "Rezervace sedí na adresu, kterou server vidí.", + "Datový tok musí projít všemi.", + "Obraz chce alternativní režim nebo DisplayPort v kabelu.", + }; + return quizyear_quiz(qy_hw2, 7, "hw2_ex8_title", "hw2_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Zrcadlo", "RAID 1", "Tip: zrcadlo přežije pád jednoho disku, ne smazání souboru.", + { + "Stejná data jsou na dvou discích.", + "Kapacita je jako jeden disk.", + "Čtení může být rychlejší, zápis ne dvojnásobný.", + "Po výměně disku se pole musí obnovit.", + NULL, + }, + }, + { + "2 / 3 • Prokládání", "RAID 0 a 5", "Tip: RAID 0 při pádu jednoho disku ztratí všechno.", + { + "RAID 0 skládá rychlost a kapacitu a nemá ochranu.", + "RAID 5 snese pád jednoho disku z aspoň tří.", + "Parita zabere kapacitu jednoho disku.", + "Obnova velkého pole trvá dlouho a druhý pád v té době je zkáza.", + NULL, + }, + }, + { + "3 / 3 • Řadič", "Hardware a software", "Tip: pole z jiného řadiče v novém serveru nemusí naběhnout.", + { + "Hardwarový řadič má vlastní cache a baterii.", + "Softwarové pole řeší systém a je přenosnější.", + "Cache bez ochrany při výpadku proudu rozbije zápis.", + "RAID nenahrazuje zálohu na jiné místo.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 0, "hw3_unit1", "hw3_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co RAID 1 přežije?", {"Pád jednoho z dvojice disků", "Smazání souboru uživatelem", "Požár místnosti", "Ransomware na obou discích naráz"}, 4, 0}, + {"Co je RAID 0?", {"Prokládání bez ochrany při pádu disku", "Zrcadlo", "Záloha na pásku", "Šifrování disku"}, 4, 0}, + {"Proč RAID není záloha?", {"Neochrání před smazáním, malwarem ani požárem skříně", "Protože neumí víc než dva disky", "Protože Windows RAID zakazuje", "Protože parita maže soubory"}, 4, 0}, + {"Co hrozí velkému poli při obnově?", {"Druhý disk spadne dřív, než se obnova dopočítá", "Pole se samo přepne na RAID 0", "BIOS smaže paritu", "NIC se při obnově nesmí točit"}, 4, 0}, + }; + static const char *hints[] = { + "Oba disky mají stejná data, včetně chyby, kterou tam zapíšete.", + "Rychlost a kapacita, nulová odolnost.", + "Záloha je další kopie jinde a v jiném čase.", + "Obnova je dlouhá a disky jsou stejně staré.", + }; + return quizyear_quiz(qy_hw3, 0, "hw3_ex1_title", "hw3_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • NAS", "Souborový server v krabici", "Tip: NAS v učebně bez zálohy je jen jeden košík na všechna data.", + { + "Nabízí složky po síti.", + "Má vlastní účty nebo se ptá školního adresáře.", + "Dva porty se dají spojit, ale jen když to umí i switch.", + "Hlučný diskový provoz nepatří do tiché učebny.", + NULL, + }, + }, + { + "2 / 3 • SAN", "Disk jako místní", "Tip: SAN nedává složku, dává blokové zařízení.", + { + "Server vidí disk, i když leží jinde.", + "Síť pro SAN se nemíchá s provozem žáků.", + "Špatné zónování vystaví disk cizímu serveru.", + "Výpadek této sítě shodí systémy, které na disku jedou.", + NULL, + }, + }, + { + "3 / 3 • Soubor", "Práva a snímky", "Tip: snímek disku není záloha, dokud leží na stejném poli.", + { + "Práva na složce mají sedět se skupinami, ne s jedním účtem učitele.", + "Snímek umí vrátit soubor smazaný dopoledne.", + "Plný svazek zastaví i zdánlivě nesouvisející službu.", + "Kapacita se hlídá dřív, než svítí červeně.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 1, "hw3_unit2", "hw3_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co NAS obvykle nabízí?", {"Složky po síti", "Blokový disk celé škole bez oprávnění", "Náhradu procesoru v noteboocích", "Obraz monitoru"}, 4, 0}, + {"Čím se SAN liší?", {"Server dostane disk jako bloky, ne jako složku", "Je to jen jiné jméno pro USB flash disk", "Jede jen po Wi-Fi", "Nemá disky"}, 4, 0}, + {"Proč snímek na stejném poli není záloha?", {"Pád pole vezme i snímek", "Snímek nejde vytvořit", "Snímek maže RAID", "SMB snímky zakazuje"}, 4, 0}, + {"Kam nepatří hlučné diskové pole?", {"Do tiché učebny vedle žáků", "Do racku s odtahem", "Do místnosti se zámek", "Do sítě oddělené od hostů"}, 4, 0}, + }; + static const char *hints[] = { + "Je to souborová služba.", + "Souborový systém si na těch blocích dělá až server.", + "Záloha musí přežít ztrátu původního úložiště.", + "Hluk a přístup jsou součást návrhu.", + }; + return quizyear_quiz(qy_hw3, 1, "hw3_ex2_title", "hw3_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tvar", "Racková jednotka", "Tip: 1U je výška, ne výkon.", + { + "Server v racku se měří v jednotkách U.", + "Kolejnice musí unést hloubku skříně.", + "Napájecí lišta není prodlužovačka z kuchyně.", + "Proud a teplo se počítají dopředu, ne až když jistič spadne.", + NULL, + }, + }, + { + "2 / 3 • Vzduch", "Studená ulička", "Tip: servery v racku sají vepředu a foukají dozadu. Otočený kus kazí ostatním.", + { + "Studený vzduch jde před servery, teplý za ně.", + "Zaslepení prázdných pozic nutí vzduch přes stroje.", + "Kabelová armáda před sáním je ucpávka.", + "Teplota v místnosti není teplota uvnitř 1U serveru.", + NULL, + }, + }, + { + "3 / 3 • Vzdáleně", "Když nejste ve škole", "Tip: vzdálená správa má vlastní port a vlastní heslo.", + { + "IPMI nebo iDRAC žije i když systém ne.", + "Patří do správní sítě, ne do učebny.", + "Konzole v racku je záloha, když síť správy spadne.", + "Štítek na serveru říká, co na něm jede.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 2, "hw3_unit3", "hw3_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co znamená 1U?", {"Výšku zařízení v racku", "Výkon jednoho jádra", "Rychlost disku", "Počet zdrojů"}, 4, 0}, + {"Kudy má server v racku sát vzduch?", {"Vepředu a foukat dozadu", "Zezadu dopředu", "Jen shora bez mřížky", "Z podlahy dírami v desce"}, 4, 0}, + {"K čemu je vzdálená správa serveru?", {"Ovládáte ho, i když systém neběží", "Nahrazuje zálohu dat", "Je to Wi-Fi pro žáky", "Maže RAID"}, 4, 0}, + {"Kam nepatří port vzdálené správy?", {"Do žákovské VLAN", "Do správní sítě", "Za firewall", "Na heslo jiné než admin na lístečku"}, 4, 0}, + }; + static const char *hints[] = { + "Je to rozměr, ne třída výkonu.", + "Otočený server ohřívá sousedům sání.", + "Vidíte obraz a umíte stroj zapnout po síti.", + "Je to klíče od serveru.", + }; + return quizyear_quiz(qy_hw3, 2, "hw3_ex3_title", "hw3_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Hypervizor", "Kdo rozděluje železo", "Tip: virtuální počítač nemá vlastní zdroj. Dělí se o ten fyzický.", + { + "Hypervizor pouští více systémů nad jedním hardwarem.", + "Každý systém má vlastní diskový soubor a virtuální síťovku.", + "Paměť, kterou slíbíte navíc, se ve špičce potká.", + "Pád fyzického serveru shodí všechny hosty naráz.", + NULL, + }, + }, + { + "2 / 3 • Síť", "Virtuální switch", "Tip: VLAN ve virtuálu musí sedět s trunkem na fyzické kartě.", + { + "Virtuální switch propojí hosty mezi sebou.", + "Most je pustí ven fyzickým portem.", + "Izolovaná síť jen mezi virtuály ven nevede.", + "Špatný trunk vystaví správní virtuál do učebny.", + NULL, + }, + }, + { + "3 / 3 • Disk", "Soubor není kouzlo", "Tip: tenký disk roste a může zaplnit svazek, na kterém leží.", + { + "Tenké provizionování slíbí víc, než je na začátku obsazeno.", + "Snímek před změnou umí vrátit systém.", + "Zapomenuté snímky zpomalí disk a zaplní úložiště.", + "Záloha virtuálu patří mimo ten samý disk.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 3, "hw3_unit4", "hw3_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co se stane, když spadne fyzický server s pěti virtuály?", {"Spadnou všechny, pokud nemají kam utéct", "Přežije aspoň správní virtuál", "Hypervizor je přenese na notebook", "Nic, virtuál zdroj nepotřebuje"}, 4, 0}, + {"K čemu je most virtuálního switche?", {"Pustí virtuál do fyzické sítě", "Smaže VLAN", "Nahradí RAID", "Zapne Secure Boot hostitele"}, 4, 0}, + {"Čím je tenký disk nebezpečný?", {"Doroste a zaplní úložiště pod sebou", "Nejde zálohovat", "Nemá souborový systém", "Vypne hypervizor"}, 4, 0}, + {"Co udělá zapomenutý snímek?", {"Roste a zpomaluje disk", "Smaže heslo BIOSu", "Opraví RAID 0", "Přidá paměť"}, 4, 0}, + }; + static const char *hints[] = { + "Sdílejí jedno železo.", + "Bez mostu virtuál ven neodejde.", + "Slíbená kapacita není volné místo.", + "Snímek je dočasná pojistka, ne trvalý režim.", + }; + return quizyear_quiz(qy_hw3, 3, "hw3_ex4_title", "hw3_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Pravidlo", "Tři místa", "Tip: kopie na tom samém disku přežije jen omyl v koši, ne pád disku.", + { + "Jedna kopie je málo, když leží u originálu.", + "Druhá má být mimo stroj a třetí mimo budovu, když na tom záleží.", + "Verze v čase vrátí soubor z pátku, ne jen ze včerejška.", + "Šifrovaná záloha bez klíče je hromada nic.", + NULL, + }, + }, + { + "2 / 3 • Co zálohovat", "Ne jen plochu", "Tip: obraz systému bez dat školy je pěkná, ale prázdná záchrana.", + { + "Data, účty a konfigurace sítě jsou různé věci.", + "Poštovní schránka v cloudu má vlastní nastavení retence.", + "Databáze se nezálohuje kopírováním otevřeného souboru naslepo.", + "Seznam, co se nezálohuje, má být vědomý.", + NULL, + }, + }, + { + "3 / 3 • Zkouška", "Obnova je součást zálohy", "Tip: záloha, kterou jste nikdy neobnovili, je hypotéza.", + { + "Jednou za čas obnovte soubor i celý systém nanečisto.", + "Měřte, jak dlouho obnova trvá.", + "Zápis říká, kdo má klíče a kde leží média.", + "Po ransomware rozhoduje stáří poslední čisté kopie.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 4, "hw3_unit5", "hw3_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč nestačí kopie na stejném disku?", {"Pád disku vezme originál i kopii", "Systém druhou složku zakazuje", "RAID ji sám smaže", "NTFS umí jen jeden soubor"}, 4, 0}, + {"Co není pořádná zkouška zálohy?", {"Jen to, že noční úloha svítí zeleně", "Obnova jednoho souboru", "Změřený čas obnovy systému", "Ověření, že klíč k šifře existuje"}, 4, 0}, + {"Co rozhoduje po ransomware?", {"Jestli existuje čistá kopie starší než nákaza", "Barva skříně", "Počet ventilátorů", "Verze spořiče obrazovky"}, 4, 0}, + {"Proč nekopírovat otevřenou databázi jako obyčejný soubor?", {"Rozpracovaný zápis bude nekonzistentní", "Databáze nemá soubor", "SMB databáze zakazuje", "Kopie by smazala RAID"}, 4, 0}, + }; + static const char *hints[] = { + "Chyba média je společná.", + "Zelená fajfka neznamená, že data jdou otevřít.", + "Šifrovaná záloha z doby, kdy už malware běžel, je taky šifrovaná.", + "Běžící databáze se zálohuje nástrojem, který umí konzistentní snímek.", + }; + return quizyear_quiz(qy_hw3, 4, "hw3_ex5_title", "hw3_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Úkol", "Překlenout výpadek", "Tip: malá UPS neutáhne učebnu. Utáhne server, než se uloží.", + { + "Při výpadku přejde na baterii.", + "Čas běhu závisí na odběru, ne na nálepce bez zátěže.", + "Server dostane signál a vypne se po pořádku.", + "Tiskárna a monitor na UPS servery okrádají o minuty.", + NULL, + }, + }, + { + "2 / 3 • Tvar", "Ne každá UPS je pro zdroj serveru", "Tip: levná UPS s obdélníkovým průběhem modernímu zdroji vadí.", + { + "Sinusový výstup je jistější pro aktivní PFC zdroje.", + "Výkon ve voltampérech není totéž co watty na štítku zdroje.", + "Přetížená UPS spadne právě ve výpadku.", + "Zkouška pod zátěží ukáže skutečné minuty.", + NULL, + }, + }, + { + "3 / 3 • Baterie", "Spotřební díl", "Tip: baterie po třech letech v teple už slíbené minuty nedá.", + { + "Test baterie nečeká na první bouřku.", + "Výměna se dělá dřív, než článek nabobtná.", + "Teplá místnost baterii ničí rychleji.", + "UPS není náhrada za zálohu dat.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 5, "hw3_unit6", "hw3_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"K čemu malá UPS u serveru je?", {"Aby se systém stihl uložit a vypnout", "Aby učebna jela celé odpoledne", "Aby nahradila zálohu disků", "Aby chladila rack"}, 4, 0}, + {"Proč na UPS nepatří laserová tiskárna?", {"Má velký odběr a sebere čas serverům", "Tiskárna UPS zničí vždy okamžitě", "USB tiskárny UPS zakazuje", "Toner potřebuje jinou frekvenci"}, 4, 0}, + {"Co ukáže jen nálepka bez zátěže?", {"Optimistický čas, který v provozu neplatí", "Přesné minuty při plném serveru", "Stav RAID", "Teplotu procesoru"}, 4, 0}, + {"Jak často baterie přestane držet slib?", {"Po letech, dřív v teple", "Nikdy, je součástí zdroje navždy", "Až po deseti minutách prvního provozu", "Jen když je UPS černá"}, 4, 0}, + }; + static const char *hints[] = { + "Jsou to minuty, ne směna.", + "Špička zapékání baterii vybíjí zbytečně.", + "Měří se s tím, co na UPS opravdu visí.", + "Je to spotřební díl a má se zkoušet.", + }; + return quizyear_quiz(qy_hw3, 5, "hw3_ex6_title", "hw3_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Co to je", "Program v železe", "Tip: firmware není ovladač ve Windows. Žije v zařízení i bez systému.", + { + "Deska, disk, switch i BIOS mají vlastní verzi.", + "Chyba ve firmware shodí zařízení bez ohledu na systém.", + "Výrobce vydává opravy i roky po prodeji.", + "Neznámá verze se nejdřív zjistí, potom se teprve stahuje.", + NULL, + }, + }, + { + "2 / 3 • Postup", "Jedno zařízení, zdroj, záloha", "Tip: aktualizace při bouřce bez UPS je způsob, jak dostat cihlu.", + { + "Přečtěte, co verze opravuje a co rozbije.", + "Záloha konfigurace switche jde bokem.", + "Napájení nesmí vypadnout v půlce zápisu.", + "Po dokončení ověřte, že naběhla i služba, ne jen dioda.", + NULL, + }, + }, + { + "3 / 3 • Řada", "Ne všechno v pátek odpoledne", "Tip: neaktualizujte jádro sítě a jediný server ve stejné hodině.", + { + "Nejdřív méně důležité zařízení.", + "Okno, kdy smí síť ležet, je domluvené.", + "Když nová verze zlobí, musíte umět předchozí.", + "Zápis verze patří k zařízení, ne do hlavy jednoho člověka.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 6, "hw3_unit7", "hw3_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se firmware liší od ovladače?", {"Je v zařízení a běží i bez operačního systému", "Je to jen soubor na ploše", "Maže se s přeinstalací vždy", "Umí ho jen tiskárna"}, 4, 0}, + {"Co před aktualizací switche uložíte?", {"Konfiguraci a zajistíte napájení", "Jen tapetu na ploše", "Heslo Wi-Fi na tabuli", "Nic, aktualizace si pamatuje sama"}, 4, 0}, + {"Proč neaktualizovat jádro a jediný server naráz?", {"Když obojí selže, nemáte z čeho učit ani spravovat", "Výrobce to technicky zakazuje vždy", "Firmware umí jen jedno zařízení na budovu", "UPS to nesežere"}, 4, 0}, + {"Co po aktualizaci ověříte?", {"Že služba opravdu naběhla, ne jen že svítí dioda", "Že je krabice teplejší", "Že se změnila barva menu", "Nic, firmware se neověřuje"}, 4, 0}, + }; + static const char *hints[] = { + "BIOS a firmware disku přežijí nový systém.", + "Přerušený zápis a chybějící záloha jsou dvě pasti.", + "Nechte si cestu zpět.", + "Start zařízení není totéž co fungující síť.", + }; + return quizyear_quiz(qy_hw3, 6, "hw3_ex7_title", "hw3_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw3_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Co měřit", "Úzké hrdlo", "Tip: rychlý procesor nezachrání pomalý disk, na kterém systém čeká.", + { + "Sledujte procesor, paměť, disk a síť zvlášť.", + "Vytížení 100 % chvíli není chyba, trvale ano.", + "Fronta na disku řekne víc než otáčky ventilátoru.", + "Jedno číslo z herního testu není návrh školního serveru.", + NULL, + }, + }, + { + "2 / 3 • Podmínky", "Stejná zkouška", "Tip: srovnávejte stejný test, stejná data a stejný čas běhu.", + { + "Studený start a druhý běh z cache nejsou totéž.", + "Notebook na baterii schválně zpomalí.", + "Antivirus v testu umí sežrat výsledek.", + "Zapište verzi systému i to, co na pozadí jelo.", + NULL, + }, + }, + { + "3 / 3 • Rozhodnutí", "Kdy dokoupit", "Tip: nejdřív paměť nebo disk, ne automaticky nejnovější procesor.", + { + "Málo paměti žene systém na disk a všechno zpomalí.", + "Plný a pomalý disk kazí i silný procesor.", + "Slabé Wi-Fi nezrychlíte výměnou grafické karty.", + "Nákup bez měření je odhad.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw3, 7, "hw3_unit8", "hw3_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw3_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co neznamená krátká špička procesoru na 100 %?", {"Že je procesor nutně slabý", "Že je všechno v pořádku navždy", "Že disk neexistuje", "Že se má hned vyměnit deska"}, 4, 0}, + {"Proč druhý běh testu bývá rychlejší?", {"Data už jsou v cache", "Procesor se sám přetaktuje natrvalo", "Disk se zmenší", "BIOS test podruhé vynechá"}, 4, 0}, + {"Co často zpomalí celý systém dřív než procesor?", {"Málo paměti a pomalý disk", "Příliš mnoho USB myš", "Barva skříně", "Počet ikon na ploše"}, 4, 0}, + {"Co k výsledku testu patří zapsat?", {"Verze, napájení a co jelo na pozadí", "Jen nejvyšší číslo", "Jméno učitele", "Heslo k BIOSu"}, 4, 0}, + }; + static const char *hints[] = { + "Záleží, jestli špička trvá a jestli na ní někdo čeká.", + "Cache není výkon železa v prvním čtení.", + "Systém pak pořád čeká na stránkování.", + "Bez podmínek číslo nejde zopakovat.", + }; + return quizyear_quiz(qy_hw3, 7, "hw3_ex8_title", "hw3_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Prašnost", "Řád, ne až když hučí", "Tip: otevřená skříň vysátá naslepo umí nasát prach dovnitř zdroje.", + { + "Filtry se perou dřív, než zčernají.", + "Zdroj se neotevírá. Čistí se zvenku.", + "Klávesnice a myš učebny jsou spotřební a špinavé.", + "Zápis, kdy se učebna dělala, příště určí interval.", + NULL, + }, + }, + { + "2 / 3 • Kontakty", "Uvolněný kabel vypadá jako mrtvá deska", "Tip: než objednáte desku, domáčkněte napájení a vyzkoušejte jiný kabel obrazu.", + { + "Oxidy na stolních konektorech kazí obraz i USB.", + "Ohnutý konektor HDMI se do portu necpe silou.", + "Napájecí kabel s poškozenou izolací se vyřadí, nelepí páskou.", + "Prodlužovačka pod kobercem je požár, ne úspora místa.", + NULL, + }, + }, + { + "3 / 3 • Evidence", "Co ve třídě vůbec je", "Tip: bez inventárního čísla nepoznáte, který zdroj se vrací do záruky.", + { + "Štítek, umístění a rok pořízení.", + "Záruka a doklad nejsou v šuplíku jednoho člověka.", + "Odepsaný stroj se nevrací do sítě s původním účtem.", + "Pravidelná prohlídka je levnější než hodina bez učebny.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 0, "hw4_unit1", "hw4_unit1_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co se při údržbě se zdrojem nedělá?", {"Neotevírá se", "Omývá se mu mřížka zvenku", "Kontroluje se kabel", "Píše se, kdy byl čištěný"}, 4, 0}, + {"Co uděláte s kabelem s porušenou izolací?", {"Vyřadíte ho", "Zalepíte izolepou a vrátíte pod koberec", "Schováte ho za skříň", "Dáte ho k žákovskému počítači"}, 4, 0}, + {"K čemu je inventární číslo?", {"Aby šlo dohledat záruku a umístění", "Aby zrychlilo procesor", "Nahrazuje heslo BIOSu", "Je to sériové číslo Windows"}, 4, 0}, + {"Kam nepatří odepsaný počítač?", {"Zpět do sítě s původními účty", "Do evidence odpadu", "K smazání disků", "Mimo žákovskou VLAN"}, 4, 0}, + }; + static const char *hints[] = { + "Uvnitř je napětí i po odpojení.", + "Poškozená izolace je požár a úraz.", + "Bez něj je reklamace hádanka.", + "Starý účet na mrtvém stroji je pořád díra, když ho někdo zapne.", + }; + return quizyear_quiz(qy_hw4, 0, "hw4_ex1_title", "hw4_quiz1_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • TPM", "Klíče mimo disk", "Tip: TPM není heslo, které uživatel píše při každém startu.", + { + "Čip drží klíče použité při startu a šifrování disku.", + "Bez něj některé systémy šifrování disku odmítnou.", + "Vymazání TPM bez zálohy klíčů znamená ztrátu přístupu k disku.", + "Na virtuálu bývá TPM také virtuální a patří k tomu stroji.", + NULL, + }, + }, + { + "2 / 3 • Start", "Řetěz důvěry", "Tip: Secure Boot a TPM řeší start, ne to, co uživatel spustí odpoledne.", + { + "Firmware ověří zavaděč.", + "Zavaděč ověří jádro.", + "Cizí systém z flash disku bez podpisu nenaběhne.", + "V laboratoři se výjimka zapíše, nenechá vypnutá navždy.", + NULL, + }, + }, + { + "3 / 3 • Zámek", "Kensington není šifra", "Tip: lankový zámek zdrží odnesení, data chrání šifrování disku.", + { + "Zámek skříně zpomalí toho, kdo chce odnést RAM.", + "Heslo BIOSu zpomalí toho, kdo chce přeházet boot.", + "Šifra disku zastaví toho, kdo disk odnese.", + "Všechny tři vrstvy řeší jiného zloděje.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 1, "hw4_unit2", "hw4_unit2_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co TPM drží?", {"Klíče pro start a šifrování", "Zálohu všech dokumentů školy", "Obraz monitoru", "Seznam VLAN"}, 4, 0}, + {"Co se stane po vymazání TPM bez zálohy klíčů?", {"Šifrovaný disk nemusí jít otevřít", "Zrychlí se start", "Smaže se jen spořič", "Nic, TPM klíče nepoužívá"}, 4, 0}, + {"Co Secure Boot nezastaví?", {"Program, který uživatel spustí v už naběhlém systému", "Nepodepsaný zavaděč", "Cizí systém z flashe, pokud podpis chybí", "Změnu řetězu startu"}, 4, 0}, + {"Co ochrání data na odcizeném disku?", {"Šifrování disku", "Jen lankový zámek skříně", "Heslo spořiče", "Inventární štítek"}, 4, 0}, + }; + static const char *hints[] = { + "Není to úložiště souborů.", + "Klíč v čipu bez kopie je pryč.", + "Hlídá start, ne každé pozdější kliknutí.", + "Zámek zdrží odnesení celé skříně, ne čtení disku na jiném stroji.", + }; + return quizyear_quiz(qy_hw4, 1, "hw4_ex2_title", "hw4_quiz2_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Účel", "Učebna není střih videa", "Tip: stejná sestava pro psaní a pro CAD je buď drahá, nebo pomalá.", + { + "Kancelář chce spolehlivost, ticho a dost paměti na prohlížeč.", + "Střih a CAD chtějí procesor, paměť a často grafiku.", + "Server chce ECC nebo aspoň diskové pole a správu, ne podsvícení.", + "Počet portů a dokování je součást ceny, ne doplněk potom.", + NULL, + }, + }, + { + "2 / 3 • Čísla", "Co srovnávat", "Tip: více jader nepomůže programu, který umí jen jedno.", + { + "Paměť berte s rezervou na čtyři roky, ne na dnešní minimum.", + "SSD jako systémový disk je základ. Velký sklad může být pomalejší.", + "Zdroj má rezervu nad součtem štítků, ne jen přesný součet.", + "Záruka na místě je u učebny levnější než odvoz dvaceti strojů.", + NULL, + }, + }, + { + "3 / 3 • Stejnost", "Jedna image", "Tip: dvacet různých desek znamená dvacet ovladačů.", + { + "Stejný model v učebně zjednoduší image i náhradní díl.", + "Dokoupit za dva roky stejnou desku často nejde. Pište, co je uvnitř.", + "Nejlevnější kus bez portů, které učebna používá, levný není.", + "Příkon a hluk patří do zadání, ne až do reklamace.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 2, "hw4_unit3", "hw4_unit3_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Podle čeho se volí sestava?", {"Podle práce, kterou na ní škola dělá", "Podle barvy krabice", "Podle nejvyššího čísla v reklamě", "Podle počtu RGB ventilátorů"}, 4, 0}, + {"Proč v učebně stejný model?", {"Jedna image a jeden náhradní díl", "Aby se lišily ovladače", "Aby každý stroj bootoval jinak", "Zákon to vyžaduje kvůli VLAN"}, 4, 0}, + {"Co u zdroje nestačí?", {"Součet štítků bez rezervy", "Rezerva nad typický odběr", "Správný formát do skříně", "Pojistka, která sedí k rozvodu"}, 4, 0}, + {"Co zdraží zdánlivě levný notebook?", {"Chybějící porty a dok, který musíte dokoupit", "To, že má SSD", "To, že má záruku", "To, že je tichý"}, 4, 0}, + }; + static const char *hints[] = { + "CAD a psaní dopisu nechtějí stejné železo.", + "Různé desky znamenají různou údržbu.", + "Špička a stárnutí se do přesného součtu nevejdou.", + "Učebna bez obrazových výstupů potřebuje doky.", + }; + return quizyear_quiz(qy_hw4, 2, "hw4_ex3_title", "hw4_quiz3_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Správa", "Cizí kapsy", "Tip: telefon žáka není školní majetek a nepatří mu školní certifikát natrvalo.", + { + "Školní tablet se dá zapsat do správy a na dálku smazat.", + "Soukromý telefon do správy školy nepatří.", + "Účet školy na soukromém zařízení má jít odebrat.", + "Ztracený školní tablet bez správy je cizí počítač s vašimi daty.", + NULL, + }, + }, + { + "2 / 3 • Síť", "Kam ho pustit", "Tip: telefon učitelů ve stejné VLAN jako servery je zbytečné riziko.", + { + "Hostovská nebo mobilní síť je oddělená.", + "Školní tablet může dostat jinou síť než návštěva.", + "Sdílené tablety se mezi žáky mažou nebo přepínají profil.", + "Nabíjení ve třídě je otázka pojistek, ne jen kabelů na stole.", + NULL, + }, + }, + { + "3 / 3 • Život", "Baterie a pád", "Tip: cyklování baterie v nabíjecím kufru ji po dvou letech unaví.", + { + "Obal a popruh jsou součást nákupu, ne charita.", + "Aktualizace systému se dělají hromadně, ne podle nálady.", + "Rozbitý displej se nevrací do oběhu s trčícím sklem.", + "Evidence IMEI nebo sériového čísla patří k inventuře.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 3, "hw4_unit4", "hw4_unit4_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se liší školní tablet od soukromého telefonu?", {"Školní jde zapsat do správy a na dálku vymazat", "Soukromý musí mít školní certifikát navždy", "Jsou to stejné režimy", "Tablet nepatří do inventury"}, 4, 0}, + {"Kam nepatří učitelský telefon?", {"Do VLAN se servery", "Do oddělené mobilní sítě", "Na účet, který jde odebrat", "Mimo žákovská data, pokud to jde"}, 4, 0}, + {"Co sdílený tablet mezi žáky potřebuje?", {"Mazání profilu nebo oddělené účty", "Jedno heslo napsané na obalu", "Trvalou složku předchozího žáka", "Vypnuté aktualizace"}, 4, 0}, + {"Co patří do evidence tabletu?", {"Sériové číslo a umístění", "Jen barva obalu", "Heslo žáka", "Soukromé fotky"}, 4, 0}, + }; + static const char *hints[] = { + "Majetek školy se dá spravovat, kapsa žáka ne.", + "Ztracený telefon nemá být klíč k serverům.", + "Cizí účet na dalším žákovi je únik.", + "Bez čísla ho nepoznáte v kupě stejných.", + }; + return quizyear_quiz(qy_hw4, 3, "hw4_ex4_title", "hw4_quiz4_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Projektor", "Lampa a síť", "Tip: lampa má hodiny. Nečekejte, až praskne uprostřed hodiny.", + { + "Zdroj obrazu a kabel jsou častější chyba než deska.", + "Filtr projektoru se ucpe prachem a přístroj se vypne.", + "Stropní držák se dotahuje, nevěší se na něj.", + "Náhradní kabel ve třídě je levnější než přestávka.", + NULL, + }, + }, + { + "2 / 3 • Tisk", "Fronta a toner", "Tip: prázdný toner a zaseknutý papír jsou dvě různé hlášky.", + { + "Sklad toneru má být suchý, ne na topení.", + "Účet tisku omezí náhodné desítky stran.", + "Síťová tiskárna se po výměně adresy musí najít znovu.", + "Servisní interval bubnu není totéž co výměna toneru.", + NULL, + }, + }, + { + "3 / 3 • Drobnosti", "Myš, sluchátka, kamera", "Tip: bezdrátová myš bez náhradní baterie je v hodině mrtvá.", + { + "Sluchátka ve třídě mají být omyvatelná.", + "Kamera na dálkovou výuku chce světlo před obličejem, ne za zády.", + "USB přijímač myši ztracený v portu projektoru se špatně hledá.", + "Spotřební věci se objednávají dřív, než dojde poslední kus.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 4, "hw4_unit5", "hw4_unit5_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co u projektoru kontrolovat dřív než desku?", {"Zdroj obrazu, kabel a filtr", "Jen typ žárovky ve stropě učebny", "Heslo Wi-Fi", "Verzi BIOSu učitelského telefonu"}, 4, 0}, + {"Čím se liší toner a buben?", {"Toner je prach s barvou, buben je díl, který obraz přenáší", "Jsou to totéž", "Buben se mění každý den", "Toner je jen u inkoustové tiskárny"}, 4, 0}, + {"Proč účty tisku?", {"Omezí náhodné velké tisky", "Zvýší rozlišení", "Nahradí papír", "Vypnou skener"}, 4, 0}, + {"Co ve třídě dochází bez náhrady jako první?", {"Baterie, kabely a spotřební díly", "Procesory", "Rackové lišty", "TPM čipy"}, 4, 0}, + }; + static const char *hints[] = { + "Obraz často končí na kabelu.", + "Prázdný toner a unavený buben vypadají na stránce jinak.", + "Fronta bez limitu tiskne omylem celé knihy.", + "Drobnost zastaví hodinu stejně jako velká porucha.", + }; + return quizyear_quiz(qy_hw4, 4, "hw4_ex5_title", "hw4_quiz5_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Disk", "Smazat neznamená vyhodit", "Tip: rychlé formátování nechá data čitelná.", + { + "Před vyřazením se disk smaže tak, aby nešel běžně obnovit.", + "Nefunkční disk se fyzicky zničí, ne jen odloží.", + "SSD se maže příkazem, který buňky opravdu uvolní.", + "Potvrzení o likvidaci patří k inventuře.", + NULL, + }, + }, + { + "2 / 3 • Železo", "Není to směsný odpad", "Tip: monitor a deska nepatří do popelnice za školou.", + { + "Elektroodpad se odevzdává na určené místo.", + "Baterie zvlášť, a nabobtnané ještě opatrněji.", + "Toner je taky odpad s režimem, ne koš ve sborovně.", + "Štítek s jménem žáka se před odvozem sundá.", + NULL, + }, + }, + { + "3 / 3 • Darování", "Starý stroj ven ze školy", "Tip: darovaný počítač bez smazání účtů je dar i s přístupy.", + { + "Nejdřív data, potom majetek.", + "Licence vázaná na školu se nesmí odstěhovat s počítačem.", + "Zápis, komu stroj připadl, chrání školu.", + "Nefunkční dar je jen přesun odpadu.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 5, "hw4_unit6", "hw4_unit6_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč nestačí rychlé formátování?", {"Data jdou často obnovit", "Formát disk fyzicky rozbije", "SSD formát zakazuje", "Smaže i TPM v jiném počítači"}, 4, 0}, + {"Kam nepatří stará deska?", {"Do směsného koše", "Do elektroodpadu", "K evidované likvidaci", "Po smazání dat, pokud se vyřazuje"}, 4, 0}, + {"Co před darem počítače uděláte?", {"Smažete data a ověříte licence", "Necháte účty, ať se nový majitel přihlásí", "Jen umyjete skříň", "Přilepíte heslo na víko"}, 4, 0}, + {"Čím se maže SSD líp než jedním přepisem naslepo?", {"Příkazem, který buňky opravdu uvolní", "Smazáním koše", "Změnou tapety", "Vypnutím Secure Boot"}, 4, 0}, + }; + static const char *hints[] = { + "Zmizí odkaz, ne obsah buněk.", + "Elektroodpad má vlastní režim.", + "Dar není předání školní sítě.", + "Řadič SSD přepis naslepo nemusí uložit tam, kde čekáte.", + }; + return quizyear_quiz(qy_hw4, 5, "hw4_ex6_title", "hw4_quiz6_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Příznak", "Co přesně nejde", "Tip: ‚nejde počítač‘ může být černý obraz, špatné heslo i spadlá síť.", + { + "Zeptejte se, co uživatel čekal a co vidí.", + "Od kdy a jestli to jde jinde.", + "Jedna učebna, nebo jen jeden stůl.", + "Bez příznaku se mění díly naslepo.", + NULL, + }, + }, + { + "2 / 3 • Izolace", "Nejužší místo", "Tip: fungující kabel a fungující port rozdělí chybu na dvě hromady.", + { + "Vyměňte kabel, port, účet nebo zařízení, vždy jen jedno.", + "Když náhradní kus funguje, původní je vinný nebo špatně nastavený.", + "Když nefunguje ani náhradní, chyba je ve společné věci.", + "Pište si mezivýsledky. Paměť po obědě lže.", + NULL, + }, + }, + { + "3 / 3 • Závěr", "Oprava a prevence", "Tip: vyměněný kabel bez zápisu se příští měsíc tváří jako záhada.", + { + "Opravte příčinu, ne jen příznak na jednom stole.", + "Řekněte uživateli, co se změnilo.", + "Díly do opravy nepatří zpátky do skříně potichu.", + "Opakovaná závada chce úpravu nákupu nebo zapojení.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 6, "hw4_unit7", "hw4_unit7_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je první otázka?", {"Co přesně nejde a od kdy", "Jakou má deska barvu", "Jestli je ve škole VLAN 20", "Kolik stojí nový notebook"}, 4, 0}, + {"Proč měnit jen jeden díl?", {"Ať víte, který rozdíl pomohl", "Sklad má jen jeden kabel", "BIOS víc výměn zakazuje", "Uživatel jinak zapomene heslo"}, 4, 0}, + {"Náhradní kabel taky nejde. Co z toho plyne?", {"Chyba je spíš ve portu, účtu nebo zařízení", "Oba kabely jsou vždy špatné", "Síť nemá smysl", "Je rozbitý TPM"}, 4, 0}, + {"Co patří do zápisu opravy?", {"Příznak, příčina a co se vyměnilo", "Jen ‚hotovo‘", "Heslo uživatele", "Soukromý názor na učitele"}, 4, 0}, + }; + static const char *hints[] = { + "Příznak zužuje vrstvu, kterou budete zkoušet.", + "Dvě výměny naráz nic nedokážou.", + "Společné selhání ukazuje na společnou část.", + "Příště podle zápisu nezačínáte od nuly.", + }; + return quizyear_quiz(qy_hw4, 6, "hw4_ex7_title", "hw4_quiz7_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +static GtkWidget *build_hw4_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tok dat", "Od kliknutí k disku", "Tip: maturita chce cestu, ne seznam zkratek.", + { + "Vstup, paměť, procesor, sběrnice, disk a výstup.", + "RAM je pracovní, disk je trvalý. Není to totéž.", + "Grafika může mít vlastní paměť a pořád jede po sběrnici.", + "Úzké hrdlo je nejpomalejší článek této cesty.", + NULL, + }, + }, + { + "2 / 3 • Spolehlivost", "Co drží učebnu", "Tip: záloha, RAID a UPS řeší tři různé nehody.", + { + "RAID drží provoz při pádu disku.", + "Záloha vrátí data z jiného místa a času.", + "UPS dá čas na vypnutí.", + "Firmware a čištění jsou prevence, ne oprava po požáru.", + NULL, + }, + }, + { + "3 / 3 • Rozhodnutí", "Proč právě tahle krabice", "Tip: umíte obhájit paměť, disk a porty pro konkrétní učebnu.", + { + "Kancelář, střih a server mají jiné priority.", + "Stejný model ve třídě šetří image.", + "Vyřazení začíná smazáním dat.", + "Závadu vedete od příznaku po jedné změně.", + NULL, + }, + }, + }; + return quizyear_unit(qy_hw4, 7, "hw4_unit8", "hw4_unit8_sub", slides, (int)(sizeof slides / sizeof slides[0])); +} + +static GtkWidget *build_hw4_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se liší RAM a disk?", {"RAM je pracovní paměť, disk drží data i bez napájení", "Jsou to totéž", "Disk je vždy rychlejší", "RAM se zálohuje na pásku"}, 4, 0}, + {"Co vrátí soubor smazaný před týdnem?", {"Záloha z dřívějšího dne, ne RAID", "RAID 1 sám", "UPS", "Secure Boot"}, 4, 0}, + {"Co dá UPS?", {"Čas na pořádné vypnutí", "Kopii dat v jiné budově", "Rychlejší procesor", "Nový disk"}, 4, 0}, + {"Čím začíná vyřazení počítače?", {"Smazáním dat", "Sundáním obalu", "Darem bez kontroly", "Zápisem do VLAN"}, 4, 0}, + }; + static const char *hints[] = { + "Vypnutí smaže obsah RAM, ne uložený soubor na disku.", + "RAID má pořád aktuální, tedy i smazaný stav.", + "Baterie není archiv.", + "Nejdřív obsah, potom plech.", + }; + return quizyear_quiz(qy_hw4, 7, "hw4_ex8_title", "hw4_quiz8_head", qs, hints, (int)(sizeof qs / sizeof qs[0])); +} + +void add_it_year_pages(GtkStack *stack) { + quizyear_boot(); + gtk_stack_add_named(stack, quizyear_map(qy_net2), "net2map"); + gtk_stack_add_named(stack, quizyear_map(qy_net3), "net3map"); + gtk_stack_add_named(stack, quizyear_map(qy_net4), "net4map"); + gtk_stack_add_named(stack, quizyear_map(qy_hw2), "hw2map"); + gtk_stack_add_named(stack, quizyear_map(qy_hw3), "hw3map"); + gtk_stack_add_named(stack, quizyear_map(qy_hw4), "hw4map"); + gtk_stack_add_named(stack, build_net2_unit1_page(), "net2unit1"); + gtk_stack_add_named(stack, build_net2_unit1_exercise_page(), "net2ex1"); + gtk_stack_add_named(stack, build_net2_unit2_page(), "net2unit2"); + gtk_stack_add_named(stack, build_net2_unit2_exercise_page(), "net2ex2"); + gtk_stack_add_named(stack, build_net2_unit3_page(), "net2unit3"); + gtk_stack_add_named(stack, build_net2_unit3_exercise_page(), "net2ex3"); + gtk_stack_add_named(stack, build_net2_unit4_page(), "net2unit4"); + gtk_stack_add_named(stack, build_net2_unit4_exercise_page(), "net2ex4"); + gtk_stack_add_named(stack, build_net2_unit5_page(), "net2unit5"); + gtk_stack_add_named(stack, build_net2_unit5_exercise_page(), "net2ex5"); + gtk_stack_add_named(stack, build_net2_unit6_page(), "net2unit6"); + gtk_stack_add_named(stack, build_net2_unit6_exercise_page(), "net2ex6"); + gtk_stack_add_named(stack, build_net2_unit7_page(), "net2unit7"); + gtk_stack_add_named(stack, build_net2_unit7_exercise_page(), "net2ex7"); + gtk_stack_add_named(stack, build_net2_unit8_page(), "net2unit8"); + gtk_stack_add_named(stack, build_net2_unit8_exercise_page(), "net2ex8"); + gtk_stack_add_named(stack, build_net3_unit1_page(), "net3unit1"); + gtk_stack_add_named(stack, build_net3_unit1_exercise_page(), "net3ex1"); + gtk_stack_add_named(stack, build_net3_unit2_page(), "net3unit2"); + gtk_stack_add_named(stack, build_net3_unit2_exercise_page(), "net3ex2"); + gtk_stack_add_named(stack, build_net3_unit3_page(), "net3unit3"); + gtk_stack_add_named(stack, build_net3_unit3_exercise_page(), "net3ex3"); + gtk_stack_add_named(stack, build_net3_unit4_page(), "net3unit4"); + gtk_stack_add_named(stack, build_net3_unit4_exercise_page(), "net3ex4"); + gtk_stack_add_named(stack, build_net3_unit5_page(), "net3unit5"); + gtk_stack_add_named(stack, build_net3_unit5_exercise_page(), "net3ex5"); + gtk_stack_add_named(stack, build_net3_unit6_page(), "net3unit6"); + gtk_stack_add_named(stack, build_net3_unit6_exercise_page(), "net3ex6"); + gtk_stack_add_named(stack, build_net3_unit7_page(), "net3unit7"); + gtk_stack_add_named(stack, build_net3_unit7_exercise_page(), "net3ex7"); + gtk_stack_add_named(stack, build_net3_unit8_page(), "net3unit8"); + gtk_stack_add_named(stack, build_net3_unit8_exercise_page(), "net3ex8"); + gtk_stack_add_named(stack, build_net4_unit1_page(), "net4unit1"); + gtk_stack_add_named(stack, build_net4_unit1_exercise_page(), "net4ex1"); + gtk_stack_add_named(stack, build_net4_unit2_page(), "net4unit2"); + gtk_stack_add_named(stack, build_net4_unit2_exercise_page(), "net4ex2"); + gtk_stack_add_named(stack, build_net4_unit3_page(), "net4unit3"); + gtk_stack_add_named(stack, build_net4_unit3_exercise_page(), "net4ex3"); + gtk_stack_add_named(stack, build_net4_unit4_page(), "net4unit4"); + gtk_stack_add_named(stack, build_net4_unit4_exercise_page(), "net4ex4"); + gtk_stack_add_named(stack, build_net4_unit5_page(), "net4unit5"); + gtk_stack_add_named(stack, build_net4_unit5_exercise_page(), "net4ex5"); + gtk_stack_add_named(stack, build_net4_unit6_page(), "net4unit6"); + gtk_stack_add_named(stack, build_net4_unit6_exercise_page(), "net4ex6"); + gtk_stack_add_named(stack, build_net4_unit7_page(), "net4unit7"); + gtk_stack_add_named(stack, build_net4_unit7_exercise_page(), "net4ex7"); + gtk_stack_add_named(stack, build_net4_unit8_page(), "net4unit8"); + gtk_stack_add_named(stack, build_net4_unit8_exercise_page(), "net4ex8"); + gtk_stack_add_named(stack, build_hw2_unit1_page(), "hw2unit1"); + gtk_stack_add_named(stack, build_hw2_unit1_exercise_page(), "hw2ex1"); + gtk_stack_add_named(stack, build_hw2_unit2_page(), "hw2unit2"); + gtk_stack_add_named(stack, build_hw2_unit2_exercise_page(), "hw2ex2"); + gtk_stack_add_named(stack, build_hw2_unit3_page(), "hw2unit3"); + gtk_stack_add_named(stack, build_hw2_unit3_exercise_page(), "hw2ex3"); + gtk_stack_add_named(stack, build_hw2_unit4_page(), "hw2unit4"); + gtk_stack_add_named(stack, build_hw2_unit4_exercise_page(), "hw2ex4"); + gtk_stack_add_named(stack, build_hw2_unit5_page(), "hw2unit5"); + gtk_stack_add_named(stack, build_hw2_unit5_exercise_page(), "hw2ex5"); + gtk_stack_add_named(stack, build_hw2_unit6_page(), "hw2unit6"); + gtk_stack_add_named(stack, build_hw2_unit6_exercise_page(), "hw2ex6"); + gtk_stack_add_named(stack, build_hw2_unit7_page(), "hw2unit7"); + gtk_stack_add_named(stack, build_hw2_unit7_exercise_page(), "hw2ex7"); + gtk_stack_add_named(stack, build_hw2_unit8_page(), "hw2unit8"); + gtk_stack_add_named(stack, build_hw2_unit8_exercise_page(), "hw2ex8"); + gtk_stack_add_named(stack, build_hw3_unit1_page(), "hw3unit1"); + gtk_stack_add_named(stack, build_hw3_unit1_exercise_page(), "hw3ex1"); + gtk_stack_add_named(stack, build_hw3_unit2_page(), "hw3unit2"); + gtk_stack_add_named(stack, build_hw3_unit2_exercise_page(), "hw3ex2"); + gtk_stack_add_named(stack, build_hw3_unit3_page(), "hw3unit3"); + gtk_stack_add_named(stack, build_hw3_unit3_exercise_page(), "hw3ex3"); + gtk_stack_add_named(stack, build_hw3_unit4_page(), "hw3unit4"); + gtk_stack_add_named(stack, build_hw3_unit4_exercise_page(), "hw3ex4"); + gtk_stack_add_named(stack, build_hw3_unit5_page(), "hw3unit5"); + gtk_stack_add_named(stack, build_hw3_unit5_exercise_page(), "hw3ex5"); + gtk_stack_add_named(stack, build_hw3_unit6_page(), "hw3unit6"); + gtk_stack_add_named(stack, build_hw3_unit6_exercise_page(), "hw3ex6"); + gtk_stack_add_named(stack, build_hw3_unit7_page(), "hw3unit7"); + gtk_stack_add_named(stack, build_hw3_unit7_exercise_page(), "hw3ex7"); + gtk_stack_add_named(stack, build_hw3_unit8_page(), "hw3unit8"); + gtk_stack_add_named(stack, build_hw3_unit8_exercise_page(), "hw3ex8"); + gtk_stack_add_named(stack, build_hw4_unit1_page(), "hw4unit1"); + gtk_stack_add_named(stack, build_hw4_unit1_exercise_page(), "hw4ex1"); + gtk_stack_add_named(stack, build_hw4_unit2_page(), "hw4unit2"); + gtk_stack_add_named(stack, build_hw4_unit2_exercise_page(), "hw4ex2"); + gtk_stack_add_named(stack, build_hw4_unit3_page(), "hw4unit3"); + gtk_stack_add_named(stack, build_hw4_unit3_exercise_page(), "hw4ex3"); + gtk_stack_add_named(stack, build_hw4_unit4_page(), "hw4unit4"); + gtk_stack_add_named(stack, build_hw4_unit4_exercise_page(), "hw4ex4"); + gtk_stack_add_named(stack, build_hw4_unit5_page(), "hw4unit5"); + gtk_stack_add_named(stack, build_hw4_unit5_exercise_page(), "hw4ex5"); + gtk_stack_add_named(stack, build_hw4_unit6_page(), "hw4unit6"); + gtk_stack_add_named(stack, build_hw4_unit6_exercise_page(), "hw4ex6"); + gtk_stack_add_named(stack, build_hw4_unit7_page(), "hw4unit7"); + gtk_stack_add_named(stack, build_hw4_unit7_exercise_page(), "hw4ex7"); + gtk_stack_add_named(stack, build_hw4_unit8_page(), "hw4unit8"); + gtk_stack_add_named(stack, build_hw4_unit8_exercise_page(), "hw4ex8"); +} + diff --git a/src/it_years_i18n.inc b/src/it_years_i18n.inc new file mode 100644 index 0000000..3bfe82d --- /dev/null +++ b/src/it_years_i18n.inc @@ -0,0 +1,198 @@ + {"net2_sub", "VLAN, STP, DHCP, DNS, NAT, IPv6, statické směrování a ACL.", "VLANs, STP, DHCP, DNS, NAT, IPv6, static routing and ACLs."}, + {"net2_unit1", "VLAN a trunk", "VLANs and trunks"}, + {"net2_unit1_sub", "Logické sítě na jednom přepínači a značkovaný spoj.", "Logical networks on one switch and a tagged link."}, + {"net2_ex1_title", "Kvíz: VLAN a trunk", "Quiz: VLANs and trunks"}, + {"net2_quiz1_head", "Kvíz k tématu VLAN a trunk", "Quiz on VLANs and trunks"}, + {"net2_unit2", "STP a smyčky", "STP and loops"}, + {"net2_unit2_sub", "Proč smyčka na druhé vrstvě shodí síť a jak ji STP přeruší.", "Why a layer-2 loop takes the network down, and how STP breaks it."}, + {"net2_ex2_title", "Kvíz: STP a smyčky", "Quiz: STP and loops"}, + {"net2_quiz2_head", "Kvíz k tématu STP a smyčky", "Quiz on STP and loops"}, + {"net2_unit3", "DHCP a DNS", "DHCP and DNS"}, + {"net2_unit3_sub", "Kdo přiděluje adresu a kdo překládá jméno.", "Who hands out an address, and who translates a name."}, + {"net2_ex3_title", "Kvíz: DHCP a DNS", "Quiz: DHCP and DNS"}, + {"net2_quiz3_head", "Kvíz k tématu DHCP a DNS", "Quiz on DHCP and DNS"}, + {"net2_unit4", "NAT a PAT", "NAT and PAT"}, + {"net2_unit4_sub", "Jedna veřejná adresa pro mnoho počítačů.", "One public address for many computers."}, + {"net2_ex4_title", "Kvíz: NAT a PAT", "Quiz: NAT and PAT"}, + {"net2_quiz4_head", "Kvíz k tématu NAT a PAT", "Quiz on NAT and PAT"}, + {"net2_unit5", "IPv6 základy", "IPv6 basics"}, + {"net2_unit5_sub", "Delší adresa, jiný zápis a co se nemění.", "A longer address, a different notation, and what stays the same."}, + {"net2_ex5_title", "Kvíz: IPv6 základy", "Quiz: IPv6 basics"}, + {"net2_quiz5_head", "Kvíz k tématu IPv6 základy", "Quiz on IPv6 basics"}, + {"net2_unit6", "Statické směrování", "Static routing"}, + {"net2_unit6_sub", "Ruční cesta, metrika a kdy přestane stačit.", "A hand-written route, the metric, and when it is no longer enough."}, + {"net2_ex6_title", "Kvíz: Statické směrování", "Quiz: Static routing"}, + {"net2_quiz6_head", "Kvíz k tématu Statické směrování", "Quiz on Static routing"}, + {"net2_unit7", "ACL a filtrování", "ACLs and filtering"}, + {"net2_unit7_sub", "Seznam, který provoz pustí a který zahodí.", "A list that permits traffic or drops it."}, + {"net2_ex7_title", "Kvíz: ACL a filtrování", "Quiz: ACLs and filtering"}, + {"net2_quiz7_head", "Kvíz k tématu ACL a filtrování", "Quiz on ACLs and filtering"}, + {"net2_unit8", "Podniková Wi-Fi", "Enterprise Wi-Fi"}, + {"net2_unit8_sub", "SSID, WPA3 a proč jedno heslo na nástěnce nestačí.", "SSIDs, WPA3, and why one password on the wall is not enough."}, + {"net2_ex8_title", "Kvíz: Podniková Wi-Fi", "Quiz: Enterprise Wi-Fi"}, + {"net2_quiz8_head", "Kvíz k tématu Podniková Wi-Fi", "Quiz on Enterprise Wi-Fi"}, + {"net3_sub", "OSPF, směrování mezi VLAN, WAN, VPN, firewall, QoS a dohled.", "OSPF, inter-VLAN routing, WAN, VPN, firewalls, QoS and monitoring."}, + {"net3_unit1", "OSPF", "OSPF"}, + {"net3_unit1_sub", "Směrovače si samy řeknou, jaké sítě znají.", "Routers tell each other which networks they know."}, + {"net3_ex1_title", "Kvíz: OSPF", "Quiz: OSPF"}, + {"net3_quiz1_head", "Kvíz k tématu OSPF", "Quiz on OSPF"}, + {"net3_unit2", "Směrování mezi VLAN", "Inter-VLAN routing"}, + {"net3_unit2_sub", "Jedna brána pro každou VLAN.", "One gateway for each VLAN."}, + {"net3_ex2_title", "Kvíz: Směrování mezi VLAN", "Quiz: Inter-VLAN routing"}, + {"net3_quiz2_head", "Kvíz k tématu Směrování mezi VLAN", "Quiz on Inter-VLAN routing"}, + {"net3_unit3", "WAN", "WAN"}, + {"net3_unit3_sub", "Spoj mezi pobočkami a k poskytovateli.", "A link between sites and towards the provider."}, + {"net3_ex3_title", "Kvíz: WAN", "Quiz: WAN"}, + {"net3_quiz3_head", "Kvíz k tématu WAN", "Quiz on WAN"}, + {"net3_unit4", "VPN", "VPN"}, + {"net3_unit4_sub", "Šifrovaný tunel přes síť, které nevěříte.", "An encrypted tunnel across a network you do not trust."}, + {"net3_ex4_title", "Kvíz: VPN", "Quiz: VPN"}, + {"net3_quiz4_head", "Kvíz k tématu VPN", "Quiz on VPN"}, + {"net3_unit5", "Firewall a zóny", "Firewalls and zones"}, + {"net3_unit5_sub", "Kdo s kým smí mluvit, ne jen jestli kabel svítí.", "Who may talk to whom, not only whether the cable is lit."}, + {"net3_ex5_title", "Kvíz: Firewall a zóny", "Quiz: Firewalls and zones"}, + {"net3_quiz5_head", "Kvíz k tématu Firewall a zóny", "Quiz on Firewalls and zones"}, + {"net3_unit6", "QoS", "QoS"}, + {"net3_unit6_sub", "Když je linka plná, někdo musí dostat přednost.", "When the link is full, someone has to go first."}, + {"net3_ex6_title", "Kvíz: QoS", "Quiz: QoS"}, + {"net3_quiz6_head", "Kvíz k tématu QoS", "Quiz on QoS"}, + {"net3_unit7", "Dohled sítě", "Watching the network"}, + {"net3_unit7_sub", "SNMP, syslog a co z grafu opravdu plyne.", "SNMP, syslog, and what a graph really tells you."}, + {"net3_ex7_title", "Kvíz: Dohled sítě", "Quiz: Watching the network"}, + {"net3_quiz7_head", "Kvíz k tématu Dohled sítě", "Quiz on Watching the network"}, + {"net3_unit8", "Záloha konfigurace", "Configuration backups"}, + {"net3_unit8_sub", "Co uložit, než se přepínač vymění.", "What to save before a switch is replaced."}, + {"net3_ex8_title", "Kvíz: Záloha konfigurace", "Quiz: Configuration backups"}, + {"net3_quiz8_head", "Kvíz k tématu Záloha konfigurace", "Quiz on Configuration backups"}, + {"net4_sub", "Návrh adres, redundance, 802.1X, hledání chyb, cloud a maturita.", "Address plans, redundancy, 802.1X, troubleshooting, cloud and the exam."}, + {"net4_unit1", "Adresní plán", "An address plan"}, + {"net4_unit1_sub", "Sítě se navrhují dřív, než se zapojí první kabel.", "Networks are designed before the first cable is patched."}, + {"net4_ex1_title", "Kvíz: Adresní plán", "Quiz: An address plan"}, + {"net4_quiz1_head", "Kvíz k tématu Adresní plán", "Quiz on An address plan"}, + {"net4_unit2", "Redundance", "Redundancy"}, + {"net4_unit2_sub", "Druhá cesta, která se opravdu použije.", "A second path that is actually used."}, + {"net4_ex2_title", "Kvíz: Redundance", "Quiz: Redundancy"}, + {"net4_quiz2_head", "Kvíz k tématu Redundance", "Quiz on Redundancy"}, + {"net4_unit3", "802.1X", "802.1X"}, + {"net4_unit3_sub", "Port pustí data, až když se zařízení představí.", "A port forwards data only after the device identifies itself."}, + {"net4_ex3_title", "Kvíz: 802.1X", "Quiz: 802.1X"}, + {"net4_quiz3_head", "Kvíz k tématu 802.1X", "Quiz on 802.1X"}, + {"net4_unit4", "Hledání chyby", "Troubleshooting"}, + {"net4_unit4_sub", "Postup zdola nahoru, ne náhodné restarty.", "Bottom-up steps, not random reboots."}, + {"net4_ex4_title", "Kvíz: Hledání chyby", "Quiz: Troubleshooting"}, + {"net4_quiz4_head", "Kvíz k tématu Hledání chyby", "Quiz on Troubleshooting"}, + {"net4_unit5", "Cloud a hybrid", "Cloud and hybrid networks"}, + {"net4_unit5_sub", "Část služeb není ve skříni na chodbě.", "Some services are no longer in the cupboard in the corridor."}, + {"net4_ex5_title", "Kvíz: Cloud a hybrid", "Quiz: Cloud and hybrid networks"}, + {"net4_quiz5_head", "Kvíz k tématu Cloud a hybrid", "Quiz on Cloud and hybrid networks"}, + {"net4_unit6", "Automatizace", "Automation"}, + {"net4_unit6_sub", "Opakovanou práci ať dělá skript, ne deset kliknutí.", "Let a script do the repeated work, not ten clicks."}, + {"net4_ex6_title", "Kvíz: Automatizace", "Quiz: Automation"}, + {"net4_quiz6_head", "Kvíz k tématu Automatizace", "Quiz on Automation"}, + {"net4_unit7", "Provoz a maturita", "Operations and the exam"}, + {"net4_unit7_sub", "Síť ve škole má pravidla, ne jen blikající porty.", "A school network has rules, not only blinking ports."}, + {"net4_ex7_title", "Kvíz: Provoz a maturita", "Quiz: Operations and the exam"}, + {"net4_quiz7_head", "Kvíz k tématu Provoz a maturita", "Quiz on Operations and the exam"}, + {"net4_unit8", "Dokumentace sítě", "Network documentation"}, + {"net4_unit8_sub", "Schéma, které sedí s tím, co je ve zdi.", "A diagram that matches what is actually in the wall."}, + {"net4_ex8_title", "Kvíz: Dokumentace sítě", "Quiz: Network documentation"}, + {"net4_quiz8_head", "Kvíz k tématu Dokumentace sítě", "Quiz on Network documentation"}, + {"hw2_sub", "Monitory, tisk, sběrnice, chlazení, sestavení, UEFI a diagnostika.", "Displays, printing, buses, cooling, assembly, UEFI and diagnostics."}, + {"hw2_unit1", "Monitory", "Displays"}, + {"hw2_unit1_sub", "Panel, rozlišení a proč 60 Hz není jediné číslo.", "The panel, the resolution, and why 60 Hz is not the only number."}, + {"hw2_ex1_title", "Kvíz: Monitory", "Quiz: Displays"}, + {"hw2_quiz1_head", "Kvíz k tématu Monitory", "Quiz on Displays"}, + {"hw2_unit2", "Tisk a sken", "Print and scan"}, + {"hw2_unit2_sub", "Laser, inkoust a síťová fronta.", "Laser, inkjet, and a network queue."}, + {"hw2_ex2_title", "Kvíz: Tisk a sken", "Quiz: Print and scan"}, + {"hw2_quiz2_head", "Kvíz k tématu Tisk a sken", "Quiz on Print and scan"}, + {"hw2_unit3", "Sběrnice a disky", "Buses and drives"}, + {"hw2_unit3_sub", "USB, SATA a NVMe nejsou zaměnitelné jen konektorem.", "USB, SATA and NVMe are not interchangeable just because of the plug."}, + {"hw2_ex3_title", "Kvíz: Sběrnice a disky", "Quiz: Buses and drives"}, + {"hw2_quiz3_head", "Kvíz k tématu Sběrnice a disky", "Quiz on Buses and drives"}, + {"hw2_unit4", "Chlazení", "Cooling"}, + {"hw2_unit4_sub", "Vzduch musí mít kudy dovnitř a ven.", "Air needs a way in and a way out."}, + {"hw2_ex4_title", "Kvíz: Chlazení", "Quiz: Cooling"}, + {"hw2_quiz4_head", "Kvíz k tématu Chlazení", "Quiz on Cooling"}, + {"hw2_unit5", "Sestavení", "Building a PC"}, + {"hw2_unit5_sub", "Pořadí, které nerozbije desku.", "An order that does not break the board."}, + {"hw2_ex5_title", "Kvíz: Sestavení", "Quiz: Building a PC"}, + {"hw2_quiz5_head", "Kvíz k tématu Sestavení", "Quiz on Building a PC"}, + {"hw2_unit6", "UEFI a boot", "UEFI and boot"}, + {"hw2_unit6_sub", "Co se děje, než naběhne systém.", "What happens before the operating system starts."}, + {"hw2_ex6_title", "Kvíz: UEFI a boot", "Quiz: UEFI and boot"}, + {"hw2_quiz6_head", "Kvíz k tématu UEFI a boot", "Quiz on UEFI and boot"}, + {"hw2_unit7", "Diagnostika", "Diagnostics"}, + {"hw2_unit7_sub", "POST, pípání a minimum, které ještě naběhne.", "POST, beep codes, and the smallest build that still starts."}, + {"hw2_ex7_title", "Kvíz: Diagnostika", "Quiz: Diagnostics"}, + {"hw2_quiz7_head", "Kvíz k tématu Diagnostika", "Quiz on Diagnostics"}, + {"hw2_unit8", "Porty a dokování", "Ports and docks"}, + {"hw2_unit8_sub", "Co z notebooku opravdu vyleze.", "What a laptop can actually drive."}, + {"hw2_ex8_title", "Kvíz: Porty a dokování", "Quiz: Ports and docks"}, + {"hw2_quiz8_head", "Kvíz k tématu Porty a dokování", "Quiz on Ports and docks"}, + {"hw3_sub", "RAID, NAS, servery, virtualizace, zálohy, UPS a firmware.", "RAID, NAS, servers, virtualisation, backups, UPS and firmware."}, + {"hw3_unit1", "RAID", "RAID"}, + {"hw3_unit1_sub", "Více disků jako jeden svazek, ne jako záloha.", "Several drives as one volume, not as a backup."}, + {"hw3_ex1_title", "Kvíz: RAID", "Quiz: RAID"}, + {"hw3_quiz1_head", "Kvíz k tématu RAID", "Quiz on RAID"}, + {"hw3_unit2", "NAS a sítě disků", "NAS and storage networks"}, + {"hw3_unit2_sub", "Disky, které nejsou uvnitř jednoho počítače.", "Drives that are not inside one computer."}, + {"hw3_ex2_title", "Kvíz: NAS a sítě disků", "Quiz: NAS and storage networks"}, + {"hw3_quiz2_head", "Kvíz k tématu NAS a sítě disků", "Quiz on NAS and storage networks"}, + {"hw3_unit3", "Servery a rack", "Servers and racks"}, + {"hw3_unit3_sub", "Skříň, do které se nedává herní počítač naležato.", "A cabinet that is not a gaming PC on its side."}, + {"hw3_ex3_title", "Kvíz: Servery a rack", "Quiz: Servers and racks"}, + {"hw3_quiz3_head", "Kvíz k tématu Servery a rack", "Quiz on Servers and racks"}, + {"hw3_unit4", "Virtualizace", "Virtualisation"}, + {"hw3_unit4_sub", "Více systémů na jednom železe.", "Several systems on one piece of hardware."}, + {"hw3_ex4_title", "Kvíz: Virtualizace", "Quiz: Virtualisation"}, + {"hw3_quiz4_head", "Kvíz k tématu Virtualizace", "Quiz on Virtualisation"}, + {"hw3_unit5", "Zálohování", "Backups"}, + {"hw3_unit5_sub", "Kopie, kterou umíte vrátit.", "A copy you can actually restore."}, + {"hw3_ex5_title", "Kvíz: Zálohování", "Quiz: Backups"}, + {"hw3_quiz5_head", "Kvíz k tématu Zálohování", "Quiz on Backups"}, + {"hw3_unit6", "UPS", "UPS"}, + {"hw3_unit6_sub", "Minuty na uložení, ne náhradní elektrárna.", "Minutes to shut down cleanly, not a spare power station."}, + {"hw3_ex6_title", "Kvíz: UPS", "Quiz: UPS"}, + {"hw3_quiz6_head", "Kvíz k tématu UPS", "Quiz on UPS"}, + {"hw3_unit7", "Firmware", "Firmware"}, + {"hw3_unit7_sub", "Aktualizace, která nejde udělat uprostřed hodiny.", "An update that is not applied in the middle of a lesson."}, + {"hw3_ex7_title", "Kvíz: Firmware", "Quiz: Firmware"}, + {"hw3_quiz7_head", "Kvíz k tématu Firmware", "Quiz on Firmware"}, + {"hw3_unit8", "Měření výkonu", "Measuring performance"}, + {"hw3_unit8_sub", "Číslo bez podmínek nic neznamená.", "A number without the conditions means nothing."}, + {"hw3_ex8_title", "Kvíz: Měření výkonu", "Quiz: Measuring performance"}, + {"hw3_quiz8_head", "Kvíz k tématu Měření výkonu", "Quiz on Measuring performance"}, + {"hw4_sub", "Údržba, TPM, nákup, mobily, periferie, ekologie a maturita.", "Maintenance, TPM, buying, mobiles, peripherals, disposal and the exam."}, + {"hw4_unit1", "Údržba", "Maintenance"}, + {"hw4_unit1_sub", "Čištění a revize dřív, než něco shoří.", "Cleaning and checks before something burns."}, + {"hw4_ex1_title", "Kvíz: Údržba", "Quiz: Maintenance"}, + {"hw4_quiz1_head", "Kvíz k tématu Údržba", "Quiz on Maintenance"}, + {"hw4_unit2", "TPM a Secure Boot", "TPM and Secure Boot"}, + {"hw4_unit2_sub", "Železo, které hlídá start systému.", "Hardware that watches the system start."}, + {"hw4_ex2_title", "Kvíz: TPM a Secure Boot", "Quiz: TPM and Secure Boot"}, + {"hw4_quiz2_head", "Kvíz k tématu TPM a Secure Boot", "Quiz on TPM and Secure Boot"}, + {"hw4_unit3", "Nákup sestavy", "Buying a computer"}, + {"hw4_unit3_sub", "Nejdřív práce, potom krabice.", "The work comes first, then the box."}, + {"hw4_ex3_title", "Kvíz: Nákup sestavy", "Quiz: Buying a computer"}, + {"hw4_quiz3_head", "Kvíz k tématu Nákup sestavy", "Quiz on Buying a computer"}, + {"hw4_unit4", "Mobilní zařízení", "Mobile devices"}, + {"hw4_unit4_sub", "Tablet a telefon ve školní síti.", "Tablets and phones on the school network."}, + {"hw4_ex4_title", "Kvíz: Mobilní zařízení", "Quiz: Mobile devices"}, + {"hw4_quiz4_head", "Kvíz k tématu Mobilní zařízení", "Quiz on Mobile devices"}, + {"hw4_unit5", "Periferie v provozu", "Peripherals in daily use"}, + {"hw4_unit5_sub", "Tiskárny, projektory a co se na nich láme.", "Printers, projectors, and what actually breaks."}, + {"hw4_ex5_title", "Kvíz: Periferie v provozu", "Quiz: Peripherals in daily use"}, + {"hw4_quiz5_head", "Kvíz k tématu Periferie v provozu", "Quiz on Peripherals in daily use"}, + {"hw4_unit6", "Ekologie a data", "Disposal and data"}, + {"hw4_unit6_sub", "Vyhozený disk pořád obsahuje školu.", "A discarded drive still contains the school."}, + {"hw4_ex6_title", "Kvíz: Ekologie a data", "Quiz: Disposal and data"}, + {"hw4_quiz6_head", "Kvíz k tématu Ekologie a data", "Quiz on Disposal and data"}, + {"hw4_unit7", "Metodika závady", "A fault method"}, + {"hw4_unit7_sub", "Jedna věta, která drží celý postup.", "One sentence that holds the whole procedure."}, + {"hw4_ex7_title", "Kvíz: Metodika závady", "Quiz: A fault method"}, + {"hw4_quiz7_head", "Kvíz k tématu Metodika závady", "Quiz on A fault method"}, + {"hw4_unit8", "Opakování k maturitě", "Exam review"}, + {"hw4_unit8_sub", "Železo, které umíte pojmenovat a zdůvodnit.", "Hardware you can name and justify."}, + {"hw4_ex8_title", "Kvíz: Opakování k maturitě", "Quiz: Exam review"}, + {"hw4_quiz8_head", "Kvíz k tématu Opakování k maturitě", "Quiz on Exam review"}, diff --git a/src/main.c b/src/main.c index 74283bd..4eec333 100644 --- a/src/main.c +++ b/src/main.c @@ -426,6 +426,8 @@ static gboolean run_smoke_test(gpointer data) { "mluve1", "mluve20", "netunit1", "netex1", "netunit10", "netex10", "netunit27", "netex27", + "net2map", "net2unit1", "net2ex1", "net4unit8", "net4ex8", + "hw2map", "hw2unit1", "hw2ex1", "hw4unit8", "hw4ex8", NULL }; const char *report = g_getenv("MATURITA_SMOKE"); @@ -532,6 +534,7 @@ void activate(GtkApplication *app, gpointer user_data) { gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears"); gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap"); + add_it_year_pages(main_stack); gtk_stack_add_named(main_stack, build_onyears_page(), "onyears"); gtk_stack_add_named(main_stack, build_onmap_page(), "onmap"); add_on_pages(main_stack); diff --git a/src/net.c b/src/net.c index b308be0..3cd859d 100644 --- a/src/net.c +++ b/src/net.c @@ -584,8 +584,21 @@ static GtkWidget *net_year_button(int year, gboolean locked) { gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); gtk_box_append(GTK_BOX(row), icon); } else { + static const char *pages[] = { + "netmap", "net2map", "net3map", "net4map", + }; + static const char *subs[] = { + "net_sub", "net2_sub", "net3_sub", "net4_sub", + }; + GtkWidget *sub = gtk_label_new(NULL); + + i18n_bind(sub, subs[year - 1], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_widget_add_css_class(sub, "quiz-intro"); + gtk_box_append(GTK_BOX(texts), sub); g_object_set_data_full(G_OBJECT(btn), "target", - g_strdup("netmap"), g_free); + g_strdup(pages[year - 1]), g_free); g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); } @@ -623,7 +636,7 @@ GtkWidget *build_netyears_page(void) { gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); for (y = 1; y <= 4; y++) - gtk_box_append(GTK_BOX(list), net_year_button(y, y > 1)); + gtk_box_append(GTK_BOX(list), net_year_button(y, FALSE)); return page; } diff --git a/src/progress.c b/src/progress.c index 2bfa8ab..9d62b71 100644 --- a/src/progress.c +++ b/src/progress.c @@ -60,6 +60,7 @@ void load_progress(void) { on4_load_progress(); en_load_progress(); de_load_progress(); + quizyear_load_all(); lit_load_progress(); sci_load_progress(); fyz_load_progress(); @@ -259,6 +260,7 @@ void progress_for_net(ProgressSum *out) { out->done_units++; } } + progress_for_net_years(out); } void hw_load_progress(void) { @@ -350,6 +352,7 @@ void progress_for_hw(ProgressSum *out) { out->done_units++; } } + progress_for_hw_years(out); } void on_load_progress(void) { diff --git a/src/quizyear.c b/src/quizyear.c new file mode 100644 index 0000000..96a7dfc --- /dev/null +++ b/src/quizyear.c @@ -0,0 +1,451 @@ +#include "graduately.h" + +/* Later years of networks and hardware. Year 1 keeps its own path. + * These years are a list of lessons, slides and a four-question quiz. */ + +#define QY_N 8 +#define QY_COUNT 6 + +struct QuizYear { + const char *prefix; + const char *map_page; + const char *back_page; + const char *title_key; + const char *sub_key; + const char *file; + NetLesson lessons[QY_N]; + char unit_page[QY_N][16]; + char ex_page[QY_N][16]; + char unit_key[QY_N][20]; + char sub_keybuf[QY_N][24]; + GtkWidget *nodes[QY_N]; +}; + +static QuizYear years[QY_COUNT]; +static gboolean qy_ready; + +QuizYear *qy_net2; +QuizYear *qy_net3; +QuizYear *qy_net4; +QuizYear *qy_hw2; +QuizYear *qy_hw3; +QuizYear *qy_hw4; + +static char map_store[QY_COUNT][16]; +static char file_store[QY_COUNT][16]; + +void quizyear_boot(void) { + static const char *prefix[] = { + "net2", "net3", "net4", "hw2", "hw3", "hw4", + }; + static const char *back[] = { + "netyears", "netyears", "netyears", + "hwyears", "hwyears", "hwyears", + }; + static const char *title[] = { + "net_year2", "net_year3", "net_year4", + "hw_year2", "hw_year3", "hw_year4", + }; + static const char *sub[] = { + "net2_sub", "net3_sub", "net4_sub", + "hw2_sub", "hw3_sub", "hw4_sub", + }; + QuizYear **slot[] = { + &qy_net2, &qy_net3, &qy_net4, &qy_hw2, &qy_hw3, &qy_hw4, + }; + + if (qy_ready) + return; + for (int y = 0; y < QY_COUNT; y++) { + QuizYear *Y = &years[y]; + + Y->prefix = prefix[y]; + Y->back_page = back[y]; + Y->title_key = title[y]; + Y->sub_key = sub[y]; + g_snprintf(map_store[y], sizeof map_store[y], "%smap", prefix[y]); + g_snprintf(file_store[y], sizeof file_store[y], "%s.conf", prefix[y]); + Y->map_page = map_store[y]; + Y->file = file_store[y]; + for (int i = 0; i < QY_N; i++) { + g_snprintf(Y->unit_page[i], sizeof Y->unit_page[i], + "%sunit%d", prefix[y], i + 1); + g_snprintf(Y->ex_page[i], sizeof Y->ex_page[i], + "%sex%d", prefix[y], i + 1); + g_snprintf(Y->unit_key[i], sizeof Y->unit_key[i], + "%s_unit%d", prefix[y], i + 1); + g_snprintf(Y->sub_keybuf[i], sizeof Y->sub_keybuf[i], + "%s_unit%d_sub", prefix[y], i + 1); + Y->lessons[i].unit_page = Y->unit_page[i]; + Y->lessons[i].ex_page = Y->ex_page[i]; + Y->lessons[i].n_slides = 3; + } + *slot[y] = Y; + } + qy_ready = TRUE; +} + +static char *qy_path(QuizYear *Y) { + return g_build_filename(app_progress_dir, Y->file, NULL); +} + +static void qy_save(QuizYear *Y) { + GKeyFile *kf = g_key_file_new(); + char *path = qy_path(Y); + gchar *data; + GError *err = NULL; + + for (int i = 0; i < QY_N; i++) { + char key[8]; + + g_snprintf(key, sizeof key, "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, Y->lessons[i].done); + } + data = g_key_file_to_data(kf, NULL, NULL); + if (!g_file_set_contents(path, data, -1, &err)) { + g_warning("quiz year: %s", err->message); + g_error_free(err); + } + g_free(data); + g_free(path); + g_key_file_free(kf); +} + +static void qy_load_one(QuizYear *Y) { + GKeyFile *kf = g_key_file_new(); + char *path = qy_path(Y); + GError *err = NULL; + + if (!g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + g_free(path); + return; + } + for (int i = 0; i < QY_N; i++) { + char key[8]; + + g_snprintf(key, sizeof key, "%d", i + 1); + Y->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + g_key_file_free(kf); + g_free(path); +} + +void quizyear_load_all(void) { + quizyear_boot(); + for (int y = 0; y < QY_COUNT; y++) + qy_load_one(&years[y]); +} + +static void qy_refresh_node(QuizYear *Y, int index) { + GtkWidget *node = Y->nodes[index]; + + if (!node) + return; + if (Y->lessons[index].done) { + gtk_widget_remove_css_class(node, "current"); + gtk_widget_add_css_class(node, "done"); + } else { + gtk_widget_remove_css_class(node, "done"); + gtk_widget_add_css_class(node, "current"); + } +} + +static void qy_mark(QuizYear *Y, int index) { + if (index < 0 || index >= QY_N || Y->lessons[index].done) + return; + Y->lessons[index].done = TRUE; + qy_save(Y); + qy_refresh_node(Y, index); + refresh_stats_ui(); +} + +static void qy_add(ProgressSum *out, int first, int n) { + for (int y = first; y < first + n; y++) { + out->total_ex += QY_N; + out->open_units += QY_N; + for (int i = 0; i < QY_N; i++) { + if (years[y].lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } + } +} + +void progress_for_net_years(ProgressSum *out) { + quizyear_boot(); + qy_add(out, 0, 3); +} + +void progress_for_hw_years(ProgressSum *out) { + quizyear_boot(); + qy_add(out, 3, 3); +} + +static void qy_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (L && L->idx > 0) { + L->idx--; + skill_slide_apply(L); + } +} + +static void qy_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + skill_slide_apply(L); + } else if (L->ex_page) { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +void quizyear_apply_lang(void) { + if (!qy_ready) + return; + for (int y = 0; y < QY_COUNT; y++) + for (int i = 0; i < QY_N; i++) + skill_slide_apply(&years[y].lessons[i]); +} + +GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key, + const char *sub_key, const NetSlide *slides, + int n_slides) { + NetLesson *L = &Y->lessons[index]; + GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + GtkWidget *scroll = gtk_scrolled_window_new(); + GtkWidget *nav; + + net_lesson_notes_ensure(L); + net_notes_target = L; + L->n_slides = (guint)n_slides; + gtk_widget_set_margin_start(page, 28); + gtk_widget_set_margin_end(page, 28); + gtk_widget_set_margin_top(page, 20); + gtk_widget_set_margin_bottom(page, 20); + gtk_box_append(GTK_BOX(page), top_bar(Y->map_page, title_key, sub_key)); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 10); + gtk_box_append(GTK_BOX(page), scroll); + L->stack = gtk_stack_new(); + gtk_stack_set_transition_type(GTK_STACK(L->stack), + GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack); + for (int s = 0; s < n_slides; s++) { + GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + char name[16]; + + g_snprintf(name, sizeof name, "slide%d", s); + gtk_widget_set_margin_top(card, 8); + gtk_widget_set_margin_bottom(card, 8); + net_note_kicker(card, slides[s].kicker); + net_note_title(card, slides[s].title); + for (int i = 0; i < 8 && slides[s].line[i]; i++) + net_note_line(card, slides[s].line[i], FALSE); + if (slides[s].tip) + net_note_line(card, slides[s].tip, TRUE); + gtk_stack_add_named(GTK_STACK(L->stack), card, name); + } + nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_set_homogeneous(GTK_BOX(nav), TRUE); + gtk_widget_set_margin_top(nav, 10); + gtk_box_append(GTK_BOX(page), nav); + L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev")); + gtk_widget_add_css_class(L->prev_btn, "btn-primary"); + gtk_box_append(GTK_BOX(nav), L->prev_btn); + g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(qy_slide_prev), L); + L->next_btn = gtk_button_new_with_label(tr("net_slide_next")); + gtk_widget_add_css_class(L->next_btn, "btn-primary"); + gtk_box_append(GTK_BOX(nav), L->next_btn); + g_signal_connect(L->next_btn, "clicked", G_CALLBACK(qy_slide_next), L); + net_rescale_lesson_notes(L, 18, 32, 15); + net_notes_target = NULL; + L->idx = 0; + skill_slide_apply(L); + return page; +} + +typedef struct { + QuizYear *year; + int index; + const ChoiceQ *qs; + int n; + int n_opts; + GtkWidget *feedback; + GtkToggleButton **toggles; + GtkWidget **hints; +} QyQuiz; + +static void qy_check(GtkButton *button, gpointer data) { + QyQuiz *ctx = data; + int ok = 0; + + (void)button; + for (int i = 0; i < ctx->n; i++) { + int active = -1; + + for (int o = 0; o < ctx->n_opts; o++) { + GtkToggleButton *tb = ctx->toggles[i * ctx->n_opts + o]; + + gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok"); + gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong"); + if (gtk_toggle_button_get_active(tb)) + active = o; + } + if (active == ctx->qs[i].correct) { + ok++; + if (active >= 0) + gtk_widget_add_css_class( + GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "ok"); + } else if (active >= 0) { + gtk_widget_add_css_class( + GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "wrong"); + } + if (ctx->hints && ctx->hints[i]) + gtk_widget_set_visible(ctx->hints[i], TRUE); + } + if (ok == ctx->n) { + set_feedback(ctx->feedback, TRUE, tr("feedback_ok")); + qy_mark(ctx->year, ctx->index); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key, + const char *head_key, const ChoiceQ *qs, + const char **hints, int n) { + QyQuiz *ctx = g_new0(QyQuiz, 1); + GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + GtkWidget *scroll = gtk_scrolled_window_new(); + GtkWidget *body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14); + GtkWidget *head = gtk_label_new(NULL); + GtkWidget *intro = gtk_label_new(NULL); + GtkWidget *check; + int n_opts = qs[0].n_options; + + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + gtk_box_append(GTK_BOX(page), + top_bar(Y->lessons[index].unit_page, title_key, NULL)); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 12); + gtk_box_append(GTK_BOX(page), scroll); + gtk_widget_set_hexpand(body, TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); + i18n_bind(head, head_key, 0); + gtk_widget_set_halign(head, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(head), TRUE); + gtk_widget_add_css_class(head, "quiz-heading"); + gtk_box_append(GTK_BOX(body), head); + i18n_bind(intro, "net_quiz_intro", 0); + gtk_widget_set_halign(intro, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(intro), TRUE); + gtk_widget_add_css_class(intro, "quiz-intro"); + gtk_box_append(GTK_BOX(body), intro); + + ctx->year = Y; + ctx->index = index; + ctx->qs = qs; + ctx->n = n; + ctx->n_opts = n_opts; + ctx->toggles = g_new0(GtkToggleButton *, n * n_opts); + ctx->hints = g_new0(GtkWidget *, n); + for (int i = 0; i < n; i++) { + GtkWidget *card = mcq_append_question(body, i + 1, &qs[i], + &ctx->toggles[i * n_opts]); + + ctx->hints[i] = meaning_add(card, hints ? hints[i] : NULL); + } + check = gtk_button_new(); + i18n_bind(check, "check", 1); + gtk_widget_add_css_class(check, "btn-primary"); + gtk_widget_set_halign(check, GTK_ALIGN_START); + gtk_widget_set_margin_top(check, 8); + gtk_box_append(GTK_BOX(body), check); + g_signal_connect(check, "clicked", G_CALLBACK(qy_check), ctx); + ctx->feedback = gtk_label_new(""); + gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(ctx->feedback), TRUE); + gtk_box_append(GTK_BOX(body), ctx->feedback); + return page; +} + +GtkWidget *quizyear_map(QuizYear *Y) { + GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + GtkWidget *scroll = gtk_scrolled_window_new(); + GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + gtk_box_append(GTK_BOX(page), top_bar(Y->back_page, Y->title_key, Y->sub_key)); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 18); + gtk_box_append(GTK_BOX(page), scroll); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(list, 520, -1); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + for (int i = 0; i < QY_N; i++) { + GtkWidget *btn = gtk_button_new(); + GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + GtkWidget *texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *title = gtk_label_new(NULL); + GtkWidget *sub = gtk_label_new(NULL); + char badge[8]; + GtkWidget *num; + + g_snprintf(badge, sizeof badge, "%d", i + 1); + num = gtk_label_new(badge); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_widget_set_margin_top(row, 10); + gtk_widget_set_margin_bottom(row, 10); + gtk_button_set_child(GTK_BUTTON(btn), row); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + i18n_bind(title, Y->unit_key[i], 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + i18n_bind(sub, Y->sub_keybuf[i], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_label_set_xalign(GTK_LABEL(sub), 0); + gtk_widget_add_css_class(sub, "quiz-intro"); + gtk_box_append(GTK_BOX(texts), sub); + g_object_set_data_full(G_OBJECT(btn), "target", + g_strdup(Y->unit_page[i]), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + Y->nodes[i] = btn; + qy_refresh_node(Y, i); + gtk_box_append(GTK_BOX(list), btn); + } + return page; +} diff --git a/src/search.c b/src/search.c index 7c5095c..b72cd54 100644 --- a/src/search.c +++ b/src/search.c @@ -188,9 +188,9 @@ static void catalog_rebuild(void) { catalog_add_key("net_years_title", "search_subject", "netyears", FALSE, 0, "site network ip"); catalog_add_key("net_year1", "search_lesson", "netmap", FALSE, 1, "rocnik"); - catalog_add_key("net_year2", "search_lesson", "netyears", TRUE, 1, "rocnik"); - catalog_add_key("net_year3", "search_lesson", "netyears", TRUE, 1, "rocnik"); - catalog_add_key("net_year4", "search_lesson", "netyears", TRUE, 1, "rocnik"); + catalog_add_key("net_year2", "search_lesson", "net2map", FALSE, 1, "rocnik"); + catalog_add_key("net_year3", "search_lesson", "net3map", FALSE, 1, "rocnik"); + catalog_add_key("net_year4", "search_lesson", "net4map", FALSE, 1, "rocnik"); for (i = 1; i <= NET_LESSONS; i++) { g_snprintf(key, sizeof(key), "net_unit%d", i); g_snprintf(page, sizeof(page), "netunit%d", i); @@ -198,11 +198,18 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "netex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + for (i = 2; i <= 4; i++) { + for (n = 1; n <= 8; n++) { + g_snprintf(key, sizeof key, "net%d_unit%d", i, n); + g_snprintf(page, sizeof page, "net%dunit%d", i, n); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "site sit"); + } + } catalog_add_key("hw_year1", "search_lesson", "hwmap", FALSE, 1, "rocnik"); - catalog_add_key("hw_year2", "search_lesson", "hwyears", TRUE, 1, "rocnik"); - catalog_add_key("hw_year3", "search_lesson", "hwyears", TRUE, 1, "rocnik"); - catalog_add_key("hw_year4", "search_lesson", "hwyears", TRUE, 1, "rocnik"); + catalog_add_key("hw_year2", "search_lesson", "hw2map", FALSE, 1, "rocnik"); + catalog_add_key("hw_year3", "search_lesson", "hw3map", FALSE, 1, "rocnik"); + catalog_add_key("hw_year4", "search_lesson", "hw4map", FALSE, 1, "rocnik"); for (i = 1; i <= HW_LESSONS; i++) { g_snprintf(key, sizeof(key), "hw_unit%d", i); g_snprintf(page, sizeof(page), "hwunit%d", i); @@ -210,6 +217,13 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "hwex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + for (i = 2; i <= 4; i++) { + for (n = 1; n <= 8; n++) { + g_snprintf(key, sizeof key, "hw%d_unit%d", i, n); + g_snprintf(page, sizeof page, "hw%dunit%d", i, n); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "hardware hw"); + } + } catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik"); catalog_add_key("on_year2", "search_lesson", "on2map", FALSE, 1, "rocnik");